001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *
010     *        http://www.apache.org/licenses/LICENSE-2.0
011     *
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License.
018     */
019    package org.apache.isis.viewer.restfulobjects.applib.util;
020    
021    import java.io.IOException;
022    import java.lang.reflect.Constructor;
023    import java.util.ArrayList;
024    import java.util.LinkedHashMap;
025    import java.util.List;
026    import java.util.Map;
027    
028    import javax.ws.rs.core.Response;
029    
030    import org.codehaus.jackson.JsonGenerationException;
031    import org.codehaus.jackson.JsonGenerator;
032    import org.codehaus.jackson.JsonNode;
033    import org.codehaus.jackson.JsonParseException;
034    import org.codehaus.jackson.JsonParser;
035    import org.codehaus.jackson.JsonProcessingException;
036    import org.codehaus.jackson.Version;
037    import org.codehaus.jackson.map.BeanProperty;
038    import org.codehaus.jackson.map.DeserializationConfig;
039    import org.codehaus.jackson.map.DeserializationContext;
040    import org.codehaus.jackson.map.DeserializerProvider;
041    import org.codehaus.jackson.map.JsonDeserializer;
042    import org.codehaus.jackson.map.JsonMappingException;
043    import org.codehaus.jackson.map.JsonSerializer;
044    import org.codehaus.jackson.map.ObjectMapper;
045    import org.codehaus.jackson.map.SerializationConfig;
046    import org.codehaus.jackson.map.SerializerProvider;
047    import org.codehaus.jackson.map.deser.BeanDeserializerFactory;
048    import org.codehaus.jackson.map.deser.JsonNodeDeserializer;
049    import org.codehaus.jackson.map.deser.StdDeserializerProvider;
050    import org.codehaus.jackson.map.module.SimpleModule;
051    import org.codehaus.jackson.type.JavaType;
052    import org.jboss.resteasy.client.ClientResponse;
053    
054    import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
055    
056    public final class JsonMapper {
057    
058        /**
059         * Provides polymorphic deserialization to any subtype of
060         * {@link JsonRepresentation}.
061         */
062        @SuppressWarnings("deprecation")
063        private static final class JsonRepresentationDeserializerFactory extends BeanDeserializerFactory {
064            @Override
065            public JsonDeserializer<Object> createBeanDeserializer(final DeserializationConfig config, final DeserializerProvider p, final JavaType type, final BeanProperty property) throws JsonMappingException {
066                final Class<?> rawClass = type.getRawClass();
067                if (JsonRepresentation.class.isAssignableFrom(rawClass)) {
068                    try {
069                        // ensure has a constructor taking a JsonNode
070                        final Constructor<?> rawClassConstructor = rawClass.getConstructor(JsonNode.class);
071                        return new JsonRepresentationDeserializer(rawClassConstructor);
072                    } catch (final SecurityException e) {
073                        // fall through
074                    } catch (final NoSuchMethodException e) {
075                        // fall through
076                    }
077                }
078                return super.createBeanDeserializer(config, p, type, property);
079            }
080    
081            private static final class JsonRepresentationDeserializer extends JsonDeserializer<Object> {
082                private final JsonDeserializer<? extends JsonNode> jsonNodeDeser = JsonNodeDeserializer.getDeserializer(JsonNode.class);
083    
084                private final Constructor<?> rawClassConstructor;
085    
086                public JsonRepresentationDeserializer(final Constructor<?> rawClassConstructor) {
087                    this.rawClassConstructor = rawClassConstructor;
088                }
089    
090                @Override
091                public JsonRepresentation deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
092                    final JsonNode jsonNode = jsonNodeDeser.deserialize(jp, ctxt);
093                    try {
094                        return (JsonRepresentation) rawClassConstructor.newInstance(jsonNode);
095                    } catch (final Exception e) {
096                        throw new IllegalStateException(e);
097                    }
098                }
099            }
100        }
101    
102        private static class JsonRepresentationSerializer extends JsonSerializer<Object> {
103            @Override
104            public void serialize(final Object value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
105                final JsonRepresentation jsonRepresentation = (JsonRepresentation) value;
106                final JsonNode jsonNode = jsonRepresentation.asJsonNode();
107                jgen.writeTree(jsonNode);
108            }
109        }
110    
111        private static ObjectMapper createObjectMapper() {
112            // it's a shame that the serialization and deserialization mechanism
113            // used aren't symmetric... but it works.
114            final DeserializerProvider deserializerProvider = new StdDeserializerProvider(new JsonRepresentationDeserializerFactory());
115            final ObjectMapper objectMapper = new ObjectMapper(null, null, deserializerProvider);
116            final SimpleModule jsonModule = new SimpleModule("json", new Version(1, 0, 0, null));
117            jsonModule.addSerializer(JsonRepresentation.class, new JsonRepresentationSerializer());
118            objectMapper.registerModule(jsonModule);
119    
120            objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
121            objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
122            return objectMapper;
123        }
124    
125        private static JsonMapper instance = new JsonMapper();
126    
127        // threadsafe
128        public final static JsonMapper instance() {
129            return instance;
130        }
131    
132        private final ObjectMapper objectMapper;
133    
134        private JsonMapper() {
135            objectMapper = createObjectMapper();
136        }
137    
138        @SuppressWarnings("unchecked")
139        public Map<String, Object> readAsMap(final String json) throws JsonParseException, JsonMappingException, IOException {
140            return read(json, LinkedHashMap.class);
141        }
142    
143        public List<?> readAsList(final String json) throws JsonParseException, JsonMappingException, IOException {
144            return read(json, ArrayList.class);
145        }
146    
147        public JsonRepresentation read(final String json) throws JsonParseException, JsonMappingException, IOException {
148            return read(json, JsonRepresentation.class);
149        }
150    
151        public <T> T read(final String json, final Class<T> requiredType) throws JsonParseException, JsonMappingException, IOException {
152            return objectMapper.readValue(json, requiredType);
153        }
154    
155        public <T> T read(final Response response, final Class<T> requiredType) throws JsonParseException, JsonMappingException, IOException {
156            final ClientResponse<?> clientResponse = (ClientResponse<?>) response; // a
157                                                                                   // shame,
158                                                                                   // but
159                                                                                   // needed
160                                                                                   // if
161                                                                                   // calling
162                                                                                   // resources
163                                                                                   // directly
164            final Object entityObj = clientResponse.getEntity(String.class);
165            if (entityObj == null) {
166                return null;
167            }
168            if (!(entityObj instanceof String)) {
169                throw new IllegalArgumentException("response entity must be a String (was " + entityObj.getClass().getName() + ")");
170            }
171            final String entity = (String) entityObj;
172    
173            return read(entity, requiredType);
174        }
175    
176        public String write(final Object object) throws JsonGenerationException, JsonMappingException, IOException {
177            return objectMapper.writeValueAsString(object);
178        }
179    
180    }