Skip to content

Commit

Permalink
[OpenLiberty#10604]: Handle Accept-Charset header in JsonBProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
andymc12 committed Jan 31, 2020
1 parent 331bb98 commit 3e32fa4
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 1 deletion.
Expand Up @@ -16,10 +16,14 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
Expand All @@ -30,6 +34,7 @@
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.ContextResolver;
Expand All @@ -38,19 +43,45 @@
import javax.ws.rs.ext.Provider;

import org.apache.cxf.jaxrs.model.ProviderInfo;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;

import com.ibm.websphere.ras.Tr;
import com.ibm.websphere.ras.TraceComponent;
import com.ibm.ws.ffdc.annotation.FFDCIgnore;

@Produces({ "*/*" })
@Consumes({ "*/*" })
@Provider
public class JsonBProvider implements MessageBodyWriter<Object>, MessageBodyReader<Object>, UnaryOperator<Jsonb> {

private final static TraceComponent tc = Tr.register(JsonBProvider.class);
private final static Charset DEFAULT_CHARSET = getDefaultCharset();
private final JsonbProvider jsonbProvider;
private final AtomicReference<Jsonb> jsonb = new AtomicReference<>();
private final Iterable<ProviderInfo<ContextResolver<?>>> contextResolvers;

@FFDCIgnore(Exception.class)
private static Charset getDefaultCharset() {
Charset cs = null;
String csStr = null;
try {
csStr = AccessController.doPrivileged((PrivilegedAction<String>)() -> {
return System.getProperty("com.ibm.ws.jaxrs.jsonbprovider.defaultCharset");
});
if (csStr != null) {
cs = Charset.forName(csStr);
}
} catch (Exception ex) {
if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
Tr.debug(tc, "Could not load specified default charset: " + csStr);
}
}
if (cs == null) {
cs = StandardCharsets.UTF_8;
}
return cs;
}


public JsonBProvider(JsonbProvider jsonbProvider, Iterable<ProviderInfo<ContextResolver<?>>> contextResolvers) {
this.contextResolvers = contextResolvers;
Expand Down Expand Up @@ -169,7 +200,7 @@ private boolean isJsonType(MediaType mediaType) {
public void writeTo(Object obj, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
String json = getJsonb().toJson(obj);
entityStream.write(json.getBytes()); // do not close
entityStream.write(json.getBytes(charset(httpHeaders))); // do not close entityStream

if (tc.isDebugEnabled()) {
Tr.debug(tc, "object=" + obj);
Expand Down Expand Up @@ -210,4 +241,18 @@ public Jsonb apply(Jsonb t) {
}
return jsonbProvider.create().build();
}

private static Charset charset(MultivaluedMap<String, Object> httpHeaders) {
List<?> charsets = httpHeaders.get(HttpHeaders.ACCEPT_CHARSET);
return JAXRSUtils.sortCharsets(charsets)
.stream()
.findFirst()
.orElseGet(() -> {
if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
Tr.debug(tc, "No matching charsets, using " + DEFAULT_CHARSET.name() +
", client requested " + charsets);
}
return DEFAULT_CHARSET;
});
}
}
Expand Up @@ -30,6 +30,8 @@
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedActionException;
Expand Down Expand Up @@ -1712,6 +1714,59 @@ public int compare(MediaType mt1, MediaType mt2) {
return types;
}

//Liberty change start
public static List<Charset> sortCharsets(List<?> charsetHeaderValues) {
if (charsetHeaderValues == null || charsetHeaderValues.size() < 1) {
return Collections.emptyList();
}
return charsetHeaderValues.stream()
.map(CharsetQualityTuple::parseTuple)
.sorted((t1, t2) -> { return Float.compare(t1.quality, t2.quality) * -1; })
.filter(t -> { return t.charset != null && t.quality > 0; })
.map(t -> { return t.charset; })
.collect(Collectors.toList());
}

private static class CharsetQualityTuple {
Charset charset;
float quality = 1; // aka weight

@FFDCIgnore(IllegalCharsetNameException.class)
static CharsetQualityTuple parseTuple(Object o) {
String s;
if (o instanceof String) {
s = (String) o;
} else {
s = o.toString();
}
CharsetQualityTuple tuple = new CharsetQualityTuple();
String[] sArr = s.split(";[qQ]=");
if (sArr.length > 1) {
try {
float f = Float.parseFloat(sArr[1]);
tuple.quality = Float.min(1.0f, Float.max(0f, f));
} catch (NumberFormatException ex) {
if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
Tr.debug(tc, "Invalid charset weight (" + s + ") - defaulting to 0.");
}
tuple.quality = 0;
}
}
try {
if (Charset.isSupported(sArr[0])) {
tuple.charset = Charset.forName(sArr[0]);
} else if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
Tr.debug(tc, "Unsupported charset, " + sArr[0]);
}
} catch (IllegalCharsetNameException ex) {
if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
Tr.debug(tc, "Illegal charset name, " + sArr[0]);
}
}
return tuple;
}
}
//Liberty change end
public static <T extends Throwable> Response convertFaultToResponse(T ex, Message currentMessage) {
return ExceptionUtils.convertFaultToResponse(ex, currentMessage);
}
Expand Down
@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2020 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package com.ibm.ws.jaxrs.utils.test;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.junit.Test;


public class JAXRSUtilsTest {


@Test
public void testSortCharsets_nullAndEmpty() throws Exception {
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(null));
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(new ArrayList<Object>()));
}

@Test
public void testSortCharsets_singleEntryNoQ() throws Exception {
String cs = "UTF-16";
List<Charset> sortedCharsets = JAXRSUtils.sortCharsets(Collections.singletonList(cs));
assertNotNull(sortedCharsets);
assertEquals(1, sortedCharsets.size());
assertEquals(Charset.forName(cs), sortedCharsets.get(0));
}

@Test
public void testSortCharsets_singleEntryWithQ() throws Exception {
String cs = "UTF-16;q=0.5";
List<Charset> sortedCharsets = JAXRSUtils.sortCharsets(Collections.singletonList(cs));
assertNotNull(sortedCharsets);
assertEquals(1, sortedCharsets.size());
assertEquals(Charset.forName("UTF-16"), sortedCharsets.get(0));

cs = "UTF-16;q=.005";
sortedCharsets = JAXRSUtils.sortCharsets(Collections.singletonList(cs));
assertNotNull(sortedCharsets);
assertEquals(1, sortedCharsets.size());
assertEquals(Charset.forName("UTF-16"), sortedCharsets.get(0));

cs = "UTF-16;q=0"; // 0 == disabled
sortedCharsets = JAXRSUtils.sortCharsets(Collections.singletonList(cs));
assertNotNull(sortedCharsets);
assertEquals(0, sortedCharsets.size());

cs = "UTF-16;q=1";
sortedCharsets = JAXRSUtils.sortCharsets(Collections.singletonList(cs));
assertNotNull(sortedCharsets);
assertEquals(1, sortedCharsets.size());
assertEquals(Charset.forName("UTF-16"), sortedCharsets.get(0));
}

@Test
public void testSortCharsets_multipleEntries() throws Exception {
List<String> charsetHeaderValues = Arrays.asList("UTF-8;q=0.3", "UTF-16BE;q=0.5", "UTF-16LE;Q=.6",
"UTF-16;q=.4", "US-ASCII;q=15.7", "ISO-8859-1;Q=-20");
List<Charset> sortedCharsets = JAXRSUtils.sortCharsets(charsetHeaderValues);
assertNotNull(sortedCharsets);
assertEquals(5, sortedCharsets.size());
assertEquals(Charset.forName("US-ASCII"), sortedCharsets.get(0));
assertEquals(Charset.forName("UTF-16LE"), sortedCharsets.get(1));
assertEquals(Charset.forName("UTF-16BE"), sortedCharsets.get(2));
assertEquals(Charset.forName("UTF-16"), sortedCharsets.get(3));
assertEquals(Charset.forName("UTF-8"), sortedCharsets.get(4));

charsetHeaderValues = Arrays.asList("UTF-8;q=0.999", "UTF-16BE", "UTF-16LE;Q=0");
sortedCharsets = JAXRSUtils.sortCharsets(charsetHeaderValues);
assertNotNull(sortedCharsets);
assertEquals(2, sortedCharsets.size());
assertEquals(Charset.forName("UTF-16BE"), sortedCharsets.get(0));
assertEquals(Charset.forName("UTF-8"), sortedCharsets.get(1));
}

@Test
public void testSortCharsets_invalidWeightEntries() throws Exception {
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(Collections.singletonList("UTF-16;q=ALOT")));
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(Collections.singletonList("UTF-16;q= ")));
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(Collections.singletonList("UTF-16;q=MILLIONS")));
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(Collections.singletonList("UTF-16;q=%20")));
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(Collections.singletonList("UTF-16 ; q = 0.5")));
assertEquals(Collections.EMPTY_LIST, JAXRSUtils.sortCharsets(Collections.singletonList("UTF-16;q==0.3")));
}
}

0 comments on commit 3e32fa4

Please sign in to comment.