StringHttpMessageConverterのカスタマイズ

## Content-Type が存在して charset が未指定（null） の場合に UTF-8 を補完する

```java
package com.example.todo.api.todo;

import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 
 * TODO: javadoc書くこと
 *
 */
public class CustomStringHttpMessageConverter extends StringHttpMessageConverter {

	/**
	 * TODO: javadoc書くこと
	 */
    public CustomStringHttpMessageConverter() {
        super(StandardCharsets.UTF_8);
    }

    /*
     * TODO: javadoc書くこと
     */
    @Override
    protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
        MediaType contentType = outputMessage.getHeaders().getContentType();

        if (contentType == null) {
            // Content-Type が未設定 → application/json + UTF-8 に設定
            outputMessage.getHeaders().setContentType(
                new MediaType("application", "json", StandardCharsets.UTF_8)
            );
        } else if (contentType.getCharset() == null) {
            // Content-Type はあるが charset が未指定 → UTF-8 を補完
            MediaType updated = new MediaType(
                contentType.getType(),
                contentType.getSubtype(),
                StandardCharsets.UTF_8
            );
            outputMessage.getHeaders().setContentType(updated);
        }

		if (!headers.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
			Long contentLength = getContentLength(str, headers.getContentType());
			if (contentLength != null) {
				headers.setContentLength(contentLength);
			}
		}
        super.writeInternal(str, outputMessage);
    }
}
```
spring-mvc-rest.xml

```xml
    <!-- コンバータを登録 -->
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="com.example.todo.api.todo.CustomStringHttpMessageConverter">
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
```

## Content-Type と charset が常に指定されている（nullや未指定のケースは存在しない）

```java
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
    MediaType contentType = outputMessage.getHeaders().getContentType();
    // すでに Content-Type + charset が設定済みと仮定しているため、何も手を加えない
    super.writeInternal(str, outputMessage);
}
```

##  Content-Typeにnull が来る可能性がある場合

```java
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException  {
	MediaType contentType = outputMessage.getHeaders().getContentType();
	if (contentType == null) {
	    outputMessage.getHeaders().setContentType(new MediaType("text", "plain", StandardCharsets.UTF_8));
	}

	if (!headers.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
		Long contentLength = getContentLength(str, headers.getContentType());
		if (contentLength != null) {
			headers.setContentLength(contentLength);
		}
	}
	super.writeInternal(str, outputMessage);
}
```

## 明示的な charset があればそれを使う。なければ UTF-8 を使う。application/json に対して無理に charset=UTF-8 を付けない。

```java
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class CustomStringHttpMessageConverter extends StringHttpMessageConverter {

    public CustomStringHttpMessageConverter() {
        super(StandardCharsets.UTF_8); // デフォルトはUTF-8
    }

    @Override
    protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
        MediaType contentType = outputMessage.getHeaders().getContentType();

        // charsetが指定されていない場合、UTF-8を補う（ただし application/json のときは無理に付けない）
        if (contentType != null && contentType.getCharset() == null) {
            if (!"application".equals(contentType.getType()) || !"json".equals(contentType.getSubtype())) {
                MediaType newContentType = new MediaType(
                    contentType.getType(),
                    contentType.getSubtype(),
                    StandardCharsets.UTF_8
                );
                outputMessage.getHeaders().setContentType(newContentType);
            }
        }

    	if (!headers.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
    		Long contentLength = getContentLength(str, headers.getContentType());
    		if (contentLength != null) {
    			headers.setContentLength(contentLength);
    		}
    	}
        super.writeInternal(str, outputMessage);
    }
}
```

----
## JUnit 4 で、Content-Type と charset が明示的に指定されたときに、カスタムコンバーターがそのまま使用することを検証するテストコード

```java
package com.example.todo.api.todo;

import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 
 * TODO: javadoc書くこと
 *
 */
public class CustomStringHttpMessageConverter extends StringHttpMessageConverter {

	/**
	 * TODO: javadoc書くこと
	 */
    public CustomStringHttpMessageConverter() {
        super(StandardCharsets.UTF_8); // デフォルトはUTF-8
    }

    /*
     * TODO: javadoc書くこと
     */
    @Override
    protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
    	if (!headers.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
    		Long contentLength = getContentLength(str, headers.getContentType());
    		if (contentLength != null) {
    			headers.setContentLength(contentLength);
    		}
    	}

        // すでに Content-Type + charset が設定済みと仮定しているため、何も手を加えない
        super.writeInternal(str, outputMessage);
    }
}
```
Junit4のテストコード

```java
package com.example.todo.api.todo;

import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;

import static org.junit.Assert.*;

public class CustomStringHttpMessageConverterTest {

    // Content-Length 再計算されることを確認
    @Test
    public void testContentLengthIsSet() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();
        String input = "123456789"; // 9バイト（UTF-8）

        MediaType mediaType = new MediaType("text", "plain", StandardCharsets.UTF_8);
        ByteArrayOutputStream body = new ByteArrayOutputStream();
        MockHttpOutputMessage output = new MockHttpOutputMessage(body, mediaType);

        converter.write(input, mediaType, output);

        long expectedLength = input.getBytes(StandardCharsets.UTF_8).length;
        assertEquals(expectedLength, output.getHeaders().getContentLength());
    }

    // Content-Type が未指定（null）なら application/json;charset=UTF-8 が補完される
    @Test
    public void testContentTypeNull_DefaultsToJsonUtf8() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();
        String input = "null content type";

        ByteArrayOutputStream body = new ByteArrayOutputStream();
        MockHttpOutputMessage output = new MockHttpOutputMessage(body, null);

        converter.write(input, null, output);

        MediaType expected = new MediaType("application", "json", StandardCharsets.UTF_8);
        assertEquals(expected, output.getHeaders().getContentType());
    }

    // Content-Type あり、charset なし → UTF-8 が補完される
    @Test
    public void testContentTypeWithoutCharset_DefaultsCharsetToUtf8() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();
        String input = "no charset";

        MediaType mediaTypeWithoutCharset = new MediaType("application", "json"); // charsetなし
        ByteArrayOutputStream body = new ByteArrayOutputStream();
        MockHttpOutputMessage output = new MockHttpOutputMessage(body, mediaTypeWithoutCharset);

        converter.write(input, mediaTypeWithoutCharset, output);

        MediaType expected = new MediaType("application", "json", StandardCharsets.UTF_8);
        assertEquals(expected, output.getHeaders().getContentType());
    }

    // Transfer-Encoding があるとき Content-Length は設定されない
    @Test
    public void testNoContentLengthWhenTransferEncodingPresent() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();
        String input = "transfer encoding test";

        Charset charset = Charset.forName("UTF-8");
        MediaType mediaType = new MediaType("application", "json", charset);
        ByteArrayOutputStream body = new ByteArrayOutputStream();

        MockHttpOutputMessage output = new MockHttpOutputMessage(body, mediaType);
        output.getHeaders().add(HttpHeaders.TRANSFER_ENCODING, "chunked");

        converter.write(input, mediaType, output);

        assertEquals(-1, output.getHeaders().getContentLength()); // -1: Springの未設定状態
    }

    // charset が windows-31j の場合、それが保持される
    @Test
    public void testCharsetWindows31jIsMaintained() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();
        String input = "日本語テスト";

        Charset windows31j = Charset.forName("windows-31j");
        MediaType mediaType = new MediaType("application", "json", windows31j);
        ByteArrayOutputStream body = new ByteArrayOutputStream();

        MockHttpOutputMessage output = new MockHttpOutputMessage(body, mediaType);

        converter.write(input, mediaType, output);

        // 実際の出力文字列が windows-31j で書かれているか確認
        String actual = new String(body.toByteArray(), windows31j);
        assertEquals(input, actual);

        // ヘッダーの Content-Type に指定 charset が維持されているか
        assertEquals(windows31j.name(), output.getHeaders().getContentType().getCharset().name());
    }


    // null を write したとき "null" として書き込まれるか確認（super.writeInternal の仕様）
    @Test
    public void testWriteNullString() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();

        ByteArrayOutputStream body = new ByteArrayOutputStream();
        MediaType mediaType = new MediaType("text", "plain", StandardCharsets.UTF_8);
        MockHttpOutputMessage output = new MockHttpOutputMessage(body, mediaType);

        converter.write(null, mediaType, output);

        String actual = body.toString(StandardCharsets.UTF_8.name());
        assertEquals("null", actual);
    }

    // 出力ストリームで IOException が発生するケース
    @Test(expected = IOException.class)
    public void testIOExceptionInOutputStream() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();

        MediaType mediaType = new MediaType("text", "plain", StandardCharsets.UTF_8);
        HttpOutputMessage output = new HttpOutputMessage() {
            @Override
            public OutputStream getBody() throws IOException {
                throw new IOException("forced error");
            }

            @Override
            public HttpHeaders getHeaders() {
                return new HttpHeaders();
            }
        };

        converter.write("test", mediaType, output);
    }

    // 不正な charset（通常は Spring 側で失敗する前提）
    @Test(expected = IllegalArgumentException.class)
    public void testInvalidCharsetInContentType() throws Exception {
        CustomStringHttpMessageConverter converter = new CustomStringHttpMessageConverter();

        // 不正な charset を指定した MediaType
        MediaType invalidCharsetMediaType = MediaType.parseMediaType("application/json; charset=not-a-charset");
        ByteArrayOutputStream body = new ByteArrayOutputStream();

        MockHttpOutputMessage output = new MockHttpOutputMessage(body, invalidCharsetMediaType);
        converter.write("test", invalidCharsetMediaType, output);
    }

    // モックの HttpOutputMessage 実装
    private static class MockHttpOutputMessage implements HttpOutputMessage {
        private final HttpHeaders headers = new HttpHeaders();
        private final ByteArrayOutputStream body;

        public MockHttpOutputMessage(ByteArrayOutputStream body, MediaType contentType) {
            this.body = body;
            if (contentType != null) {
                this.headers.setContentType(contentType);
            }
        }

        @Override
        public OutputStream getBody() {
            return body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }
    }
}
```