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 */
019package org.apache.isis.viewer.restfulobjects.applib.util;
020
021import java.io.IOException;
022import java.lang.reflect.Constructor;
023import java.util.ArrayList;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.ws.rs.core.Response;
029
030import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
031import org.codehaus.jackson.JsonGenerationException;
032import org.codehaus.jackson.JsonGenerator;
033import org.codehaus.jackson.JsonNode;
034import org.codehaus.jackson.JsonParseException;
035import org.codehaus.jackson.JsonParser;
036import org.codehaus.jackson.JsonProcessingException;
037import org.codehaus.jackson.Version;
038import org.codehaus.jackson.map.BeanProperty;
039import org.codehaus.jackson.map.DeserializationConfig;
040import org.codehaus.jackson.map.DeserializationContext;
041import org.codehaus.jackson.map.DeserializerProvider;
042import org.codehaus.jackson.map.JsonDeserializer;
043import org.codehaus.jackson.map.JsonMappingException;
044import org.codehaus.jackson.map.JsonSerializer;
045import org.codehaus.jackson.map.ObjectMapper;
046import org.codehaus.jackson.map.SerializationConfig;
047import org.codehaus.jackson.map.SerializerProvider;
048import org.codehaus.jackson.map.deser.BeanDeserializerFactory;
049import org.codehaus.jackson.map.deser.JsonNodeDeserializer;
050import org.codehaus.jackson.map.deser.StdDeserializerProvider;
051import org.codehaus.jackson.map.module.SimpleModule;
052import org.codehaus.jackson.type.JavaType;
053import org.jboss.resteasy.client.ClientResponse;
054
055public final class JsonMapper {
056
057    /**
058     * Provides polymorphic deserialization to any subtype of
059     * {@link JsonRepresentation}.
060     */
061    @SuppressWarnings("deprecation")
062    private static final class JsonRepresentationDeserializerFactory extends BeanDeserializerFactory {
063        @Override
064        public JsonDeserializer<Object> createBeanDeserializer(final DeserializationConfig config, final DeserializerProvider p, final JavaType type, final BeanProperty property) throws JsonMappingException {
065            final Class<?> rawClass = type.getRawClass();
066            if (JsonRepresentation.class.isAssignableFrom(rawClass)) {
067                try {
068                    // ensure has a constructor taking a JsonNode
069                    final Constructor<?> rawClassConstructor = rawClass.getConstructor(JsonNode.class);
070                    return new JsonRepresentationDeserializer(rawClassConstructor);
071                } catch (final SecurityException e) {
072                    // fall through
073                } catch (final NoSuchMethodException e) {
074                    // fall through
075                }
076            }
077            return super.createBeanDeserializer(config, p, type, property);
078        }
079
080        private static final class JsonRepresentationDeserializer extends JsonDeserializer<Object> {
081            private final JsonDeserializer<? extends JsonNode> jsonNodeDeser = JsonNodeDeserializer.getDeserializer(JsonNode.class);
082
083            private final Constructor<?> rawClassConstructor;
084
085            public JsonRepresentationDeserializer(final Constructor<?> rawClassConstructor) {
086                this.rawClassConstructor = rawClassConstructor;
087            }
088
089            @Override
090            public JsonRepresentation deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
091                final JsonNode jsonNode = jsonNodeDeser.deserialize(jp, ctxt);
092                try {
093                    return (JsonRepresentation) rawClassConstructor.newInstance(jsonNode);
094                } catch (final Exception e) {
095                    throw new IllegalStateException(e);
096                }
097            }
098        }
099    }
100
101    private static class JsonRepresentationSerializer extends JsonSerializer<Object> {
102        @Override
103        public void serialize(final Object value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
104            final JsonRepresentation jsonRepresentation = (JsonRepresentation) value;
105            final JsonNode jsonNode = jsonRepresentation.asJsonNode();
106            jgen.writeTree(jsonNode);
107        }
108    }
109
110    private static ObjectMapper createObjectMapper() {
111        // it's a shame that the serialization and deserialization mechanism
112        // used aren't symmetric... but it works.
113        final DeserializerProvider deserializerProvider = new StdDeserializerProvider(new JsonRepresentationDeserializerFactory());
114        final ObjectMapper objectMapper = new ObjectMapper(null, null, deserializerProvider);
115        final SimpleModule jsonModule = new SimpleModule("json", new Version(1, 0, 0, null));
116        jsonModule.addSerializer(JsonRepresentation.class, new JsonRepresentationSerializer());
117        objectMapper.registerModule(jsonModule);
118
119        objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
120        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
121        return objectMapper;
122    }
123
124    private static JsonMapper instance = new JsonMapper();
125
126    // threadsafe
127    public final static JsonMapper instance() {
128        return instance;
129    }
130
131    private final ObjectMapper objectMapper;
132
133    private JsonMapper() {
134        objectMapper = createObjectMapper();
135    }
136
137    @SuppressWarnings("unchecked")
138    public Map<String, Object> readAsMap(final String json) throws JsonParseException, JsonMappingException, IOException {
139        return read(json, LinkedHashMap.class);
140    }
141
142    public List<?> readAsList(final String json) throws JsonParseException, JsonMappingException, IOException {
143        return read(json, ArrayList.class);
144    }
145
146    public JsonRepresentation read(final String json) throws JsonParseException, JsonMappingException, IOException {
147        return read(json, JsonRepresentation.class);
148    }
149
150    public <T> T read(final String json, final Class<T> requiredType) throws JsonParseException, JsonMappingException, IOException {
151        return objectMapper.readValue(json, requiredType);
152    }
153
154    public <T> T read(final Response response, final Class<T> requiredType) throws JsonParseException, JsonMappingException, IOException {
155        final ClientResponse<?> clientResponse = (ClientResponse<?>) response; // a
156                                                                               // shame,
157                                                                               // but
158                                                                               // needed
159                                                                               // if
160                                                                               // calling
161                                                                               // resources
162                                                                               // directly
163        final Object entityObj = clientResponse.getEntity(String.class);
164        if (entityObj == null) {
165            return null;
166        }
167        if (!(entityObj instanceof String)) {
168            throw new IllegalArgumentException("response entity must be a String (was " + entityObj.getClass().getName() + ")");
169        }
170        final String entity = (String) entityObj;
171
172        return read(entity, requiredType);
173    }
174
175    public String write(final Object object) throws JsonGenerationException, JsonMappingException, IOException {
176        return objectMapper.writeValueAsString(object);
177    }
178
179}