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());
+    }
+}


Reply via email to