Author: davidb Date: Mon Jun 13 16:26:19 2016 New Revision: 1748282 URL: http://svn.apache.org/viewvc?rev=1748282&view=rev Log: Felix Converter - some refactoring
Added: felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/ felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonCodecImpl.java felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonDecodingImpl.java felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonEncodingImpl.java felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/ felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONCodecTest.java felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONSerializationTest.java Removed: felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/JsonCodecImpl.java felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/JsonDecodingImpl.java felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/JsonEncodingImpl.java felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/JSONSerializationTest.java felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/JsonCodecTest.java Added: felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonCodecImpl.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonCodecImpl.java?rev=1748282&view=auto ============================================================================== --- felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonCodecImpl.java (added) +++ felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonCodecImpl.java Mon Jun 13 16:26:19 2016 @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.converter.impl.json; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.converter.impl.ConverterImpl; +import org.osgi.service.converter.Codec; +import org.osgi.service.converter.Converter; +import org.osgi.service.converter.Decoding; +import org.osgi.service.converter.Encoding; +import org.osgi.service.converter.TypeReference; + +public class JsonCodecImpl implements Codec { + private Map<String, Object> configuration = new ConcurrentHashMap<>(); + private ThreadLocal<Boolean> threadLocal = new ThreadLocal<>(); + private Converter converter = new ConverterImpl(); // TODO inject? + + @Override + public Codec with(Converter c) { + converter = c; + return this; + } + + @Override + public <T> Decoding<T> decode(Class<T> cls) { + return new JsonDecodingImpl<T>(converter, cls); + } + + @Override + public Encoding encode(Object obj) { + Encoding encoding = new JsonEncodingImpl(converter, configuration, obj); + + if (pretty()) { + Boolean top = threadLocal.get(); + if (top == null) { + threadLocal.set(Boolean.TRUE); + + encoding = new EncodingWrapper("{}{}{}{}{}", encoding, "{}{}{}{}{}"); + } + } + return encoding; + } + + private boolean pretty() { + return Boolean.TRUE.equals(Boolean.parseBoolean((String) configuration.get("pretty"))); + } + + private class EncodingWrapper implements Encoding { + private final Encoding delegate; + private String prefix; + private String postfix; + + EncodingWrapper(String pre, Encoding encoding, String post) { + prefix = pre; + delegate = encoding; + postfix = post; + } + + @Override + public void to(OutputStream os) { + try { + os.write(toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + try { + return prefix + delegate.toString() + postfix; + } finally { + threadLocal.set(null); + } + } + + @Override + public Encoding pretty() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void to(OutputStream out, Charset charset) { + // TODO Auto-generated method stub + + } + + @Override + public Appendable to(Appendable out) { + // TODO Auto-generated method stub + return null; + } + } + + @Override + public <T> Decoding<T> decode(TypeReference<T> ref) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Decoding<?> decode(Type type) { + // TODO Auto-generated method stub + return null; + } +} Added: felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonDecodingImpl.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonDecodingImpl.java?rev=1748282&view=auto ============================================================================== --- felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonDecodingImpl.java (added) +++ felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonDecodingImpl.java Mon Jun 13 16:26:19 2016 @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.converter.impl.json; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.service.converter.Converter; +import org.osgi.service.converter.Decoding; + +public class JsonDecodingImpl<T> implements Decoding<T> { + private final Class<T> clazz; + private final Converter converter; + + public JsonDecodingImpl(Converter c, Class<T> cls) { + converter = c; + clazz = cls; + } + + @Override + public T from(CharSequence in) { + if (Map.class.isAssignableFrom(clazz)) { + return createMapFromJSONString(in); + } + return deserializeSingleJSONValue(clazz, in); + } + + private T createMapFromJSONString(CharSequence in) { + Map m = new HashMap(); + String s = in.toString().trim(); + if (!s.startsWith("{") || !s.endsWith("}")) + throw new IllegalArgumentException("JSON Should start and end with '{' and '}': " + s); + + // Eat braces + s = s.substring(1, s.length() - 1); + + int commaIdx = -1; + do { + int colonIdx = s.indexOf(':'); + if (colonIdx <= 0) + throw new IllegalArgumentException("JSON Should contain key-value pairs: " + s); + + String key = s.substring(0, colonIdx).trim(); + if (!key.startsWith("\"") || !key.endsWith("\"")) + throw new IllegalArgumentException("JSON key should be double-quoted: " + s); + key = key.substring(1, key.length() - 1); + + // move to after ':' + s = s.substring(colonIdx + 1); + + commaIdx = getNextComma(s); + String val; + if (commaIdx > 0) { + val = s.substring(0, commaIdx); + + // move to after ',' + s = s.substring(commaIdx + 1); + } else { + val = s; + } + + + val = val.trim(); + Object parsed; + if (val.startsWith("{")) { + parsed = new JsonCodecImpl().decode(Map.class).from(val); + } else { + if ("null".equals(val)) + parsed = null; + else if ("true".equals(val)) + parsed = true; + else if ("false".equals(val)) + parsed = false; + else if (val.startsWith("\"") && val.endsWith("\"")) + parsed = val.substring(1, val.length() - 1); + else if (val.contains(".")) + parsed = Double.valueOf(val); + else + parsed = Long.valueOf(val); + } + m.put(key, parsed); + } while (commaIdx > 0); + + return (T) m; + } + + private int getNextComma(String s) { + int bracelevel = 0; + for (int i=0; i<s.length(); i++) { + switch(s.charAt(i)) { + case '{': bracelevel++; + break; + case '}': bracelevel--; + break; + case ',': if (bracelevel == 0) return i; + break; + } + } + return -1; + } + + @SuppressWarnings("unchecked") + private <T> T deserializeSingleJSONValue(Class<T> cls, CharSequence cs) { + try { + Method m = cls.getDeclaredMethod("valueOf", String.class); + if (m != null) { + return (T) m.invoke(null, cs); + } + } catch (Exception e) { + return null; + } + return null; + } + + @Override + public T from(InputStream in) { + // TODO Auto-generated method stub + return null; + } + + @Override + public T from(InputStream in, Charset charset) { + // TODO Auto-generated method stub + return null; + } + + @Override + public T from(Readable in) { + // TODO Auto-generated method stub + return null; + } +} Added: felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonEncodingImpl.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonEncodingImpl.java?rev=1748282&view=auto ============================================================================== --- felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonEncodingImpl.java (added) +++ felix/trunk/converter/src/main/java/org/apache/felix/converter/impl/json/JsonEncodingImpl.java Mon Jun 13 16:26:19 2016 @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.converter.impl.json; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.osgi.service.converter.Converter; +import org.osgi.service.converter.Encoding; + +public class JsonEncodingImpl implements Encoding { + private final Converter converter; + private final Map<String, Object> configuration; + private final Object object; + + JsonEncodingImpl(Converter c, Map<String, Object> cfg, Object obj) { + converter = c; + configuration = cfg; + object = obj; + } + + private boolean ignoreNull() { + return Boolean.TRUE.equals(Boolean.parseBoolean((String) configuration.get("ignoreNull"))); + } + + @Override + public void to(OutputStream os) { + try { + os.write(encode(object).getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return encode(object); + } + + @SuppressWarnings("rawtypes") + public String encode(Object obj) { + if (obj == null) { + return ignoreNull() ? "" : "null"; + } + + if (obj instanceof Map) { + return encodeMap((Map) obj); + } else if (obj instanceof Collection) { + return encodeCollection((Collection) obj); + } else if (obj.getClass().isArray()) { + return encodeCollection(asCollection(obj)); + } else if (obj instanceof Number) { + return obj.toString(); + } else if (obj instanceof Boolean) { + return obj.toString(); + } + + return "\"" + converter.convert(obj).to(String.class) + "\""; + } + + private Collection<?> asCollection(Object arr) { + // Arrays.asList() doesn't work for primitive arrays + int len = Array.getLength(arr); + List<Object> l = new ArrayList<>(len); + for (int i=0; i<len; i++) { + l.add(Array.get(arr, i)); + } + return l; + } + + private String encodeCollection(Collection<?> collection) { + StringBuilder sb = new StringBuilder("["); + + boolean first = true; + for (Object o : collection) { + if (first) + first = false; + else + sb.append(','); + + sb.append(encode(o)); + } + + sb.append("]"); + return sb.toString(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private String encodeMap(Map m) { + StringBuilder sb = new StringBuilder("{"); + for (Entry<?,?> entry : (Set<Entry>) m.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) + if (ignoreNull()) + continue; + + if (sb.length() > 1) + sb.append(','); + sb.append('"'); + sb.append(entry.getKey().toString()); + sb.append("\":"); + sb.append(encode(entry.getValue())); + } + sb.append("}"); + + return sb.toString(); + } + + @Override + public Encoding pretty() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void to(OutputStream out, Charset charset) { + // TODO Auto-generated method stub + + } + + @Override + public Appendable to(Appendable out) { + // TODO Auto-generated method stub + return null; + } +} Added: felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONCodecTest.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONCodecTest.java?rev=1748282&view=auto ============================================================================== --- felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONCodecTest.java (added) +++ felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONCodecTest.java Mon Jun 13 16:26:19 2016 @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.converter.impl.json; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.converter.impl.ConverterImpl; +import org.apache.felix.converter.impl.json.JsonCodecImpl; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.commons.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.service.converter.Adapter; +import org.osgi.service.converter.Converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class JSONCodecTest { + private Converter converter; + + @Before + public void setUp() { + converter = new ConverterImpl(); + } + + @After + public void tearDown() { + converter = null; + } + + @Test + public void testJSONCodec() throws Exception { + Map<Object, Object> m1 = new HashMap<>(); + m1.put("x", true); + m1.put("y", null); + Map<Object, Object> m = new HashMap<>(); + m.put(1, 11L); + m.put("ab", "cd"); + m.put(true, m1); + + JsonCodecImpl jsonCodec = new JsonCodecImpl(); + String json = jsonCodec.encode(m).toString(); + + JSONObject jo = new JSONObject(json); + assertEquals(11, jo.getInt("1")); + assertEquals("cd", jo.getString("ab")); + JSONObject jo2 = jo.getJSONObject("true"); + assertEquals(true, jo2.getBoolean("x")); + assertTrue(jo2.isNull("y")); + + @SuppressWarnings("rawtypes") + Map m2 = jsonCodec.decode(Map.class).from(json); + // m2 is not exactly equal to m, as the keys are all strings now, this is unavoidable with JSON + assertEquals(m.size(), m2.size()); + assertEquals(m.get(1), m2.get("1")); + assertEquals(m.get("ab"), m2.get("ab")); + assertEquals(m.get(true), m2.get("true")); + } + + @Test + public void testCodecWithAdapter() throws JSONException { + Map<String, Foo> m1 = new HashMap<>(); + m1.put("f", new Foo("fofofo")); + Map<String, Object> m = new HashMap<>(); + m.put("submap", m1); + + Adapter ca = converter.getAdapter(); + ca.rule(Foo.class, String.class, Foo::tsFun, v -> Foo.fsFun(v)); + + JsonCodecImpl jsonCodec = new JsonCodecImpl(); + String json = jsonCodec.with(ca).encode(m).toString(); + + JSONObject jo = new JSONObject(json); + assertEquals(1, jo.length()); + JSONObject jo1 = jo.getJSONObject("submap"); + assertEquals("<fofofo>", jo1.getString("f")); + + // TODO convert back into a Map<String, Foo> via TypeReference + } + + static class Foo { + private final String val; + + public Foo(String s) { + val = s; + } + + public String tsFun() { + return "<" + val + ">"; + } + + public static Foo fsFun(String s) { + return new Foo(s.substring(1, s.length() - 1)); + } + } +} Added: felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONSerializationTest.java URL: http://svn.apache.org/viewvc/felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONSerializationTest.java?rev=1748282&view=auto ============================================================================== --- felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONSerializationTest.java (added) +++ felix/trunk/converter/src/test/java/org/apache/felix/converter/impl/json/JSONSerializationTest.java Mon Jun 13 16:26:19 2016 @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.converter.impl.json; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.converter.impl.json.JsonCodecImpl; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class JSONSerializationTest { + @Test + public void testComplexMapSerialization() { + Map<String, Object> m = new HashMap<>(); + m.put("sKey", "a string"); + m.put("iKey", 42); + m.put("bKey", true); + m.put("noKey", null); + m.put("simpleArray", new int[] {1,2,3}); + + Map<String, Object> m1 = new HashMap<>(); + m1.put("a", 1L); + m1.put("b", "hello"); + m.put("simpleObject", m1); + + String expected = "{\"bKey\":true," + + "\"simpleArray\":[1,2,3]," + + "\"iKey\":42," + + "\"sKey\":\"a string\"," + + "\"simpleObject\":{\"a\":1,\"b\":\"hello\"}," + + "\"noKey\":null}"; + assertEquals(expected, new JsonCodecImpl().encode(m).toString()); + } + + @Test + public void testComplexMapSerialization2() { + Map<String, Object> m2 = new HashMap<>(); + m2.put("yes", Boolean.TRUE); + m2.put("no", Collections.singletonMap("maybe", false)); + + Map<String, Object> cm = new HashMap<>(); + cm.put("list", Arrays.asList( + Collections.singletonMap("x", "y"), + Collections.singletonMap("x", "b"))); + cm.put("embedded", m2); + + String expected = "{\"list\":[{\"x\":\"y\"},{\"x\":\"b\"}]," + + "\"embedded\":" + + "{\"no\":{\"maybe\":false},\"yes\":true}}"; + assertEquals(expected, new JsonCodecImpl().encode(cm).toString()); + } +}