001package ball.databind;
002/*-
003 * ##########################################################################
004 * Data Binding Utilities
005 * $Id: PolymorphicTypeMap.java 6118 2020-06-04 19:31:45Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-databind/trunk/src/main/java/ball/databind/PolymorphicTypeMap.java $
007 * %%
008 * Copyright (C) 2016 - 2020 Allen D. Ball
009 * %%
010 * Licensed under the Apache License, Version 2.0 (the "License");
011 * you may not use this file except in compliance with the License.
012 * You may obtain a copy of the License at
013 *
014 *      http://www.apache.org/licenses/LICENSE-2.0
015 *
016 * Unless required by applicable law or agreed to in writing, software
017 * distributed under the License is distributed on an "AS IS" BASIS,
018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019 * See the License for the specific language governing permissions and
020 * limitations under the License.
021 * ##########################################################################
022 */
023import ball.util.PropertiesImpl;
024import com.fasterxml.jackson.core.JsonParser;
025import com.fasterxml.jackson.core.ObjectCodec;
026import com.fasterxml.jackson.databind.BeanDescription;
027import com.fasterxml.jackson.databind.DeserializationConfig;
028import com.fasterxml.jackson.databind.DeserializationContext;
029import com.fasterxml.jackson.databind.JsonDeserializer;
030import com.fasterxml.jackson.databind.JsonNode;
031import com.fasterxml.jackson.databind.deser.AbstractDeserializer;
032import com.fasterxml.jackson.databind.deser.BeanDeserializer;
033import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
034import com.fasterxml.jackson.databind.node.TreeTraversingParser;
035import java.beans.BeanInfo;
036import java.beans.PropertyDescriptor;
037import java.io.IOException;
038import java.io.InputStream;
039import java.util.ArrayList;
040import java.util.Collection;
041import java.util.TreeMap;
042import java.util.TreeSet;
043import lombok.ToString;
044import org.apache.commons.lang3.StringUtils;
045
046import static java.beans.Introspector.getBeanInfo;
047import static java.util.Comparator.comparing;
048import static java.util.Objects.requireNonNull;
049
050/**
051 * Class suitable for mapping polymorphic subtypes.  Subclasses can specify
052 * the mapping in a {@link java.util.Properties} resource which will be
053 * automatically loaded on instantiation.
054 *
055 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
056 * @version $Revision: 6118 $
057 */
058public abstract class PolymorphicTypeMap extends TreeMap<Class<?>,Class<?>[]> {
059    private static final long serialVersionUID = -4465979259676184876L;
060
061    /**
062     * Sole constructor.
063     */
064    protected PolymorphicTypeMap() {
065        super(comparing(Class::getName));
066
067        try {
068            PropertiesImpl properties = new PropertiesImpl();
069            String name = getClass().getSimpleName() + ".properties";
070            InputStream in = getClass().getResourceAsStream(name);
071
072            if (in != null) {
073                try {
074                    properties.load(in);
075                } finally {
076                    in.close();
077                }
078            }
079
080            ClassLoader loader = getClass().getClassLoader();
081            Package pkg = getClass().getPackage();
082
083            for (String key : properties.stringPropertyNames()) {
084                TreeSet<Class<?>> value =
085                    new TreeSet<>(comparing(Class::getName));
086
087                for (String substring :
088                         properties.getProperty(key)
089                         .split("[,\\p{Space}]+")) {
090                    substring = substring.trim();
091
092                    if (! StringUtils.isEmpty(substring)) {
093                        value.add(getClassFor(loader, pkg, substring));
094                    }
095                }
096
097                put(getClassFor(loader, pkg, key),
098                    value.toArray(new Class<?>[] { }));
099            }
100        } catch (Exception exception) {
101            throw new ExceptionInInitializerError(exception);
102        }
103    }
104
105    private Class<?> getClassFor(ClassLoader loader,
106                                 Package pkg, String name) throws Exception {
107        Class<?> cls = null;
108
109        try {
110            cls = Class.forName(pkg.getName() + "." + name, true, loader);
111        } catch (Exception exception) {
112            cls = Class.forName(name, true, loader);
113        }
114
115        return cls;
116    }
117
118    /**
119     * Method to get a {@link BeanDeserializerModifier} for this
120     * {@link PolymorphicTypeMap}.
121     *
122     * @return  The {@link BeanDeserializerModifier}.
123     */
124    public BeanDeserializerModifier getBeanDeserializerModifier() {
125        return new DeserializerModifier();
126    }
127
128    /**
129     * Callback from {@link JsonDeserializer} implementation to allow
130     * subclass implementations to initialize the resulting bean with the
131     * parse-tree ({@link JsonNode}).
132     *
133     * @param   object          The {@link Object} to initialize.
134     * @param   codec           The {@link ObjectCodec}.
135     * @param   node            The {@link JsonNode}.
136     *
137     * @throws  IOException     If there is a problem initializing the
138     *                          {@link JSONBean}.
139     */
140    protected void initialize(Object object,
141                              ObjectCodec codec,
142                              JsonNode node) throws IOException {
143    }
144
145    @Override
146    public Class<?>[] put(Class<?> key, Class<?>[] value) {
147        for (Class<?> subtype : value) {
148            if (! key.isAssignableFrom(subtype)) {
149                throw new IllegalArgumentException(subtype.getName()
150                                                   + " is not a subclass of "
151                                                   + key.getName());
152            }
153        }
154
155        return super.put(key, value);
156    }
157
158    @ToString
159    private class DeserializerModifier extends BeanDeserializerModifier {
160        public DeserializerModifier() { super(); }
161
162        @Override
163        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
164                                                      BeanDescription description,
165                                                      JsonDeserializer<?> deserializer) {
166            Class<?> key = description.getBeanClass();
167
168            if (containsKey(key)) {
169                deserializer =
170                    (deserializer instanceof BeanDeserializer)
171                        ? new BeanDeserializerImpl((BeanDeserializer) deserializer,
172                                                   key, get(key))
173                        : new AbstractDeserializerImpl(description,
174                                                       key, get(key));
175            }
176
177            return deserializer;
178        }
179
180        private class BeanInfoList extends ArrayList<BeanInfo> {
181            private static final long serialVersionUID = 1008165295877024242L;
182
183            private final Class<?> supertype;
184
185            public BeanInfoList(Class<?> supertype, Class<?>... subtypes) {
186                super(subtypes.length);
187
188                this.supertype = requireNonNull(supertype, "supertype");
189
190                try {
191                    for (Class<?> subtype : subtypes) {
192                        if (supertype.isAssignableFrom(subtype)) {
193                            add(getBeanInfo(subtype, supertype));
194                        } else {
195                            throw new IllegalArgumentException(subtype.getName()
196                                                               + " is not a subclass of "
197                                                               + supertype.getName());
198                        }
199                    }
200                } catch (Exception exception) {
201                    throw new ExceptionInInitializerError(exception);
202                }
203            }
204
205            public Class<?> supertype() { return supertype; }
206
207            public Class<?> subtypeFor(JsonNode node) {
208                Class<?> subtype = null;
209
210                for (BeanInfo info : this) {
211                    if (hasAll(node, info.getPropertyDescriptors())) {
212                        subtype =
213                            info.getBeanDescriptor()
214                            .getBeanClass()
215                            .asSubclass(supertype);
216                        break;
217                    }
218                }
219
220                return subtype;
221            }
222
223            private boolean hasAll(JsonNode node,
224                                   PropertyDescriptor... properties) {
225                boolean hasAll = true;
226
227                for (PropertyDescriptor property : properties) {
228                    hasAll &= node.has(property.getName());
229
230                    if (! hasAll) {
231                        break;
232                    }
233                }
234
235                return hasAll;
236            }
237        }
238
239        @ToString
240        private class JsonParserImpl extends TreeTraversingParser {
241            protected JsonNode node = null;
242
243            public JsonParserImpl(JsonParser parser) throws IOException {
244                this((JsonNode) parser.readValueAsTree(), parser.getCodec());
245            }
246
247            private JsonParserImpl(JsonNode node, ObjectCodec codec) {
248                super(node, codec);
249
250                this.node = requireNonNull(node, "node");
251            }
252        }
253
254        @ToString
255        private class BeanDeserializerImpl extends BeanDeserializer {
256            private static final long serialVersionUID = 7600217898656123257L;
257
258            private final BeanInfoList list;
259
260            public BeanDeserializerImpl(BeanDeserializer deserializer,
261                                        Class<?> supertype,
262                                        Class<?>... subtypes) {
263                super(deserializer);
264
265                list = new BeanInfoList(supertype, subtypes);
266            }
267
268            @Override
269            public Class<?> handledType() { return list.supertype(); }
270
271            @Override
272            public Object deserialize(JsonParser parser,
273                                      DeserializationContext context) throws IOException {
274                Object object = null;
275                ObjectCodec codec = parser.getCodec();
276                JsonNode node = null;
277
278                if (parser instanceof JsonParserImpl) {
279                    node = ((JsonParserImpl) parser).node;
280
281                    if (node != null) {
282                        ((JsonParserImpl) parser).node = null;
283                    }
284                }
285
286                if (node != null) {
287                    Class<?> subtype = list.subtypeFor(node);
288
289                    if (subtype != null) {
290                        object = codec.readValue(parser, subtype);
291                    } else {
292                        object = super.deserialize(parser, context);
293                    }
294
295                    initialize(list.supertype().cast(object), codec, node);
296                } else {
297                    object =
298                        codec.readValue(new JsonParserImpl(parser),
299                                        list.supertype());
300                }
301
302                return object;
303            }
304        }
305
306        @ToString
307        private class AbstractDeserializerImpl extends AbstractDeserializer {
308            private static final long serialVersionUID = -7887072343275693448L;
309
310            private final BeanInfoList list;
311
312            public AbstractDeserializerImpl(BeanDescription description,
313                                            Class<?> supertype,
314                                            Class<?>... subtypes) {
315                super(description);
316
317                list = new BeanInfoList(supertype, subtypes);
318            }
319
320            @Override
321            public Class<?> handledType() { return list.supertype(); }
322
323            @Override
324            public Object deserialize(JsonParser parser,
325                                      DeserializationContext context) throws IOException {
326                Object object = null;
327                ObjectCodec codec = parser.getCodec();
328                JsonNode node = null;
329
330                if (parser instanceof JsonParserImpl) {
331                    node = ((JsonParserImpl) parser).node;
332
333                    if (node != null) {
334                        ((JsonParserImpl) parser).node = null;
335                    }
336                }
337
338                if (node != null) {
339                    Class<?> subtype = list.subtypeFor(node);
340
341                    if (subtype != null) {
342                        object = codec.readValue(parser, subtype);
343                    } else {
344                        object = super.deserialize(parser, context);
345                    }
346
347                    initialize(list.supertype().cast(object), codec, node);
348                } else {
349                    object =
350                        codec.readValue(new JsonParserImpl(parser),
351                                        list.supertype());
352                }
353
354                return object;
355            }
356        }
357    }
358}