diff options
author | Jim Hahn <jrh3@att.com> | 2019-02-06 13:14:57 -0500 |
---|---|---|
committer | Jim Hahn <jrh3@att.com> | 2019-02-08 17:18:45 -0500 |
commit | 4ec725ef0905cd5490ed71b6576fdc1ef8fef17e (patch) | |
tree | 45fa7a22bbb586b3c6bf3821dcd4618403dbe9e5 /gson/src | |
parent | 7f1be8710503b4c34a49c96be8a6818757499bd6 (diff) |
Add superclasses for gson-jackson migration
Added common classes needed by other gson-jackson code.
Modified some logic to make it more maintainable or perform better.
Updated comments and spacing.
Fix another comment.
Moved gson classes from utils to a separate gson project.
Added GsonXxx annotations to mirror jackson annotations.
Removed unneeded dependencies from gson pom.
Removed old GsonMessage class from policy-endpoints.
Removed trailing spaces.
Updated licenses.
Removed more trailing spaces.
Removed unneeded checkstyle suppression file from utils.
Change-Id: I1a285500faeb0a0b6a1467d09b92ecd3cded713e
Issue-ID: POLICY-1428
Signed-off-by: Jim Hahn <jrh3@att.com>
Diffstat (limited to 'gson/src')
14 files changed, 2717 insertions, 0 deletions
diff --git a/gson/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java b/gson/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java new file mode 100644 index 00000000..2112c97c --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.Gson; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +/** + * Provider that serializes and de-serializes JSON via gson. + */ +@Provider +@Consumes(MediaType.WILDCARD) +@Produces(MediaType.WILDCARD) +public class GsonMessageBodyHandler implements MessageBodyReader<Object>, MessageBodyWriter<Object> { + + /** + * Object to be used to serialize and de-serialize. + */ + private Gson gson; + + /** + * Constructs the object, using a plain Gson object. + */ + public GsonMessageBodyHandler() { + this(new Gson()); + } + + /** + * Constructs the object. + * + * @param gson the Gson object to be used to serialize and de-serialize + */ + public GsonMessageBodyHandler(Gson gson) { + this.gson = gson; + } + + @Override + public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return canHandle(mediaType); + } + + @Override + public long getSize(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + + try (OutputStreamWriter writer = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) { + Type jsonType = (type.equals(genericType) ? type : genericType); + gson.toJson(object, jsonType, writer); + } + } + + @Override + public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return canHandle(mediaType); + } + + /** + * Determines if this provider can handle the given media type. + * + * @param mediaType the media type of interest + * @return {@code true} if this provider handles the given media type, {@code false} + * otherwise + */ + private boolean canHandle(MediaType mediaType) { + if (mediaType == null) { + return true; + } + + String subtype = mediaType.getSubtype(); + + return "json".equalsIgnoreCase(subtype) || subtype.endsWith("+json") || "javascript".equals(subtype) + || "x-javascript".equals(subtype) || "x-json".equals(subtype); + } + + @Override + public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap<String, String> httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + + try (InputStreamReader streamReader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) { + Type jsonType = (type.equals(genericType) ? type : genericType); + return gson.fromJson(streamReader, jsonType); + } + } +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java b/gson/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java new file mode 100644 index 00000000..cb959c43 --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java @@ -0,0 +1,104 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.JsonElement; +import java.lang.reflect.GenericArrayType; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Excludes all fields from serialization/deserialization, if the class is managed. + */ +public class JacksonExclusionStrategy implements ExclusionStrategy { + + /** + * Classes that are explicitly not managed by the GSON jackson adapters. + */ + // @formatter:off + private static final Set<Class<?>> unmanaged = new HashSet<>(Arrays.asList( + boolean.class, + byte.class, + short.class, + int.class, + long.class, + float.class, + double.class, + char.class, + Boolean.class, + Byte.class, + Short.class, + Integer.class, + Long.class, + Float.class, + Double.class, + Character.class, + String.class)); + // @formatter:on + + /** + * Classes whose subclasses are explicitly not managed by the GSON jackson adapters. + */ + // @formatter:off + private static final Set<Class<?>> unmanagedSuper = new HashSet<>(Arrays.asList( + GenericArrayType.class, + Map.class, + Collection.class, + JsonElement.class)); + // @formatter:on + + @Override + public boolean shouldSkipField(FieldAttributes attrs) { + return isManaged(attrs.getDeclaringClass()); + } + + @Override + public boolean shouldSkipClass(Class<?> clazz) { + return false; + } + + /** + * Determines if a class is managed by this adapter, which typically means that it is + * <i>not</i> a generic class such as {@link JsonElement} or some type of collection. + * + * @param clazz the class to be examined + * @return {@code true} if the class is managed by this adapter, {@code false} + * otherwise + */ + public static boolean isManaged(Class<?> clazz) { + if (clazz.isArray() || clazz.isEnum() || clazz.isPrimitive() || unmanaged.contains(clazz)) { + return false; + } + + for (Class<?> sup : unmanagedSuper) { + if (sup.isAssignableFrom(clazz)) { + return false; + } + } + + return true; + } +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java new file mode 100644 index 00000000..859f5386 --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonAnyGetter annotation, but used by gson. This requires the gson + * object to be configured with the jackson default behaviors (i.e., the associated + * JacksonXxx strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface GsonJsonAnyGetter { + +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java new file mode 100644 index 00000000..87e0f330 --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonAnySetter annotation, but used by gson. This requires the gson + * object to be configured with the jackson default behaviors (i.e., the associated + * JacksonXxx strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface GsonJsonAnySetter { + +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java new file mode 100644 index 00000000..cf2d4394 --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonIgnore annotation, but used by gson. This requires the gson object + * to be configured with the jackson default behaviors (i.e., the associated JacksonXxx + * strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target({FIELD, METHOD}) +public @interface GsonJsonIgnore { + +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java new file mode 100644 index 00000000..c31c19bb --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonProperty annotation, but used by gson. This requires the gson object + * to be configured with the jackson default behaviors (i.e., the associated JacksonXxx + * strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target({FIELD, METHOD}) +public @interface GsonJsonProperty { + + /** + * Property name of this item when placed into a JsonObject. + * @return the item's serialized name + */ + String value() default ""; +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/internal/Adapter.java b/gson/src/main/java/org/onap/policy/common/gson/internal/Adapter.java new file mode 100644 index 00000000..b4ef53f7 --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/internal/Adapter.java @@ -0,0 +1,339 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +/** + * Super class of adapters used to serialize and de-serialize an item. + */ +public class Adapter { + + /** + * Pattern to match valid identifiers. + */ + private static final Pattern VALID_NAME_PAT = Pattern.compile("[a-zA-Z_]\\w*"); + + /** + * Name of the property within the json structure containing the item. + */ + private final String propName; + + /** + * Gson object that will provide the type converter. + */ + private final Gson gson; + + /** + * Converter used when reading. + */ + private final ConvInfo reader; + + /** + * Converter used when writing, allocated lazily, once an actual type is determined. + */ + private volatile ConvInfo writer = null; + + /** + * Name of the item being lifted - used when throwing exceptions. + */ + private final String fullName; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param field field used to access the item from within an object + */ + public Adapter(Gson gson, Field field) { + this.propName = detmPropName(field); + this.reader = new ConvInfo(TypeToken.get(field.getGenericType())); + this.gson = gson; + this.fullName = getQualifiedName(field); + + field.setAccessible(true); + } + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param accessor method used to access the item from within an object + * @param forGetter {@code true} if the name is for a "getter" method, {@code false} + * if for a "setter" + * @param valueType the class of value on which this operates + */ + public Adapter(Gson gson, Method accessor, boolean forGetter, Type valueType) { + this.propName = (forGetter ? detmGetterPropName(accessor) : detmSetterPropName(accessor)); + this.reader = new ConvInfo(TypeToken.get(valueType)); + this.gson = gson; + this.fullName = getQualifiedName(accessor); + + accessor.setAccessible(true); + } + + /** + * Converts an object to a json tree. + * + * @param object the object to be converted + * @return a json tree representing the object + */ + @SuppressWarnings("unchecked") + public JsonElement toJsonTree(Object object) { + // always use a converter for the specific subclass + Class<? extends Object> clazz = object.getClass(); + + if (writer == null) { + // race condition here, but it's ok to overwrite a previous value + writer = new ConvInfo(TypeToken.get(clazz)); + } + + ConvInfo wtr = writer; + TypeAdapter<Object> conv = + (TypeAdapter<Object>) (wtr.clazz == clazz ? wtr.getConverter() : gson.getAdapter(clazz)); + + return conv.toJsonTree(object); + } + + /** + * Converts a json tree to an object. + * + * @param tree the tree to be converted + * @return the object represented by the tree + */ + public Object fromJsonTree(JsonElement tree) { + return reader.getConverter().fromJsonTree(tree); + } + + public final String getPropName() { + return propName; + } + + public final String getFullName() { + return fullName; + } + + /** + * Makes an error message, appending the item's full name to the message prefix. + * + * @param prefix the message prefix + * @return the error message + */ + public String makeError(String prefix) { + return (prefix + fullName); + } + + /** + * Determines if the field is managed by the walker. + * + * @param field the field to examine + * @return {@code true} if the field is managed by the walker, {@code false} otherwise + */ + public static boolean isManaged(Field field) { + return VALID_NAME_PAT.matcher(field.getName()).matches(); + } + + /** + * Determines if the method is managed by the walker. + * + * @param method the method to examine + * @return {@code true} if the method is managed by the walker, {@code false} + * otherwise + */ + public static boolean isManaged(Method method) { + return VALID_NAME_PAT.matcher(method.getName()).matches(); + } + + /** + * Determines the property name of an item within the json structure. + * + * @param field the item within the object + * @return the json property name for the item or {@code null} if the name is invalid + */ + public static String detmPropName(Field field) { + // use the serialized name, if specified + GsonJsonProperty prop = field.getAnnotation(GsonJsonProperty.class); + if (prop != null && !prop.value().isEmpty()) { + return prop.value(); + } + + // no name provided - use it as is + return (isManaged(field) ? field.getName() : null); + } + + /** + * Determines the property name of an item, within the json structure, associated with + * a "get" method. + * + * @param method method to be invoked to get the item within the object + * @return the json property name for the item, or {@code null} if the method name is + * not valid + */ + public static String detmGetterPropName(Method method) { + + return detmPropNameCommon(method, () -> { + + if (!isManaged(method)) { + return null; + } + + String name = method.getName(); + + if (name.startsWith("get")) { + return name.substring(3); + + } else if (name.startsWith("is")) { + Class<?> treturn = method.getReturnType(); + + if (treturn == boolean.class || treturn == Boolean.class) { + return name.substring(2); + } + } + + // not a valid name for a "getter" method + return null; + }); + } + + /** + * Determines the property name of an item, within the json structure, associated with + * a "set" method. + * + * @param method method to be invoked to set the item within the object + * @return the json property name for the item, or {@code null} if the method name is + * not valid + */ + public static String detmSetterPropName(Method method) { + + return detmPropNameCommon(method, () -> { + + if (!isManaged(method)) { + return null; + } + + String name = method.getName(); + + if (name.startsWith("set")) { + return name.substring(3); + } + + // not a valid name for a "setter" method + return null; + }); + } + + /** + * Determines the property name of an item within the json structure. + * + * @param method method to be invoked to get/set the item within the object + * @param extractor function to extract the name directly from the method name + * @return the json property name for the item, or {@code null} if the method name is + * not valid + */ + private static String detmPropNameCommon(Method method, Supplier<String> extractor) { + + // use the property name, if specified + GsonJsonProperty propName = method.getAnnotation(GsonJsonProperty.class); + if (propName != null && !propName.value().isEmpty()) { + return propName.value(); + } + + // no name provided - must compute it from the method name + String name = extractor.get(); + + if (name == null || name.isEmpty()) { + // nothing left after stripping the prefix - invalid name + return null; + } + + // translate the first letter to lower-case + return name.substring(0, 1).toLowerCase() + name.substring(1); + } + + /** + * Gets the fully qualified name of a field. + * + * @param field field whose name is desired + * @return the field fully qualified name + */ + public static String getQualifiedName(Field field) { + return (field.getDeclaringClass().getName() + "." + field.getName()); + } + + /** + * Gets the fully qualified name of a method. + * + * @param method method whose name is desired + * @return the method's fully qualified name + */ + public static String getQualifiedName(Method method) { + return (method.getDeclaringClass().getName() + "." + method.getName()); + } + + /** + * Converter info. + */ + private class ConvInfo { + + /** + * Type on which the converter works. + */ + private TypeToken<?> type; + + /** + * Class of object on which the converter works. + */ + private Class<?> clazz; + + /** + * Converter to use, initialized lazily. + */ + private volatile TypeAdapter<?> conv = null; + + /** + * Constructs the object. + * + * @param type type of object to be converted + */ + public ConvInfo(TypeToken<?> type) { + this.type = type; + this.clazz = type.getRawType(); + } + + public final TypeAdapter<?> getConverter() { + if (conv == null) { + // race condition here, but it's ok to overwrite a previous value + this.conv = gson.getAdapter(type); + } + + return conv; + } + } +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java b/gson/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java new file mode 100644 index 00000000..e985d98a --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java @@ -0,0 +1,389 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.JsonParseException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter; +import org.onap.policy.common.gson.annotation.GsonJsonAnySetter; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +/** + * Data populated while walking the hierarchy of a class. + */ +public class ClassWalker { + + public static final String ANY_GETTER_MISMATCH_ERR = + GsonJsonAnyGetter.class.getSimpleName() + " parameter mismatch for: "; + + public static final String ANY_SETTER_MISMATCH_ERR = + GsonJsonAnySetter.class.getSimpleName() + " parameter mismatch for: "; + + public static final String ANY_SETTER_TYPE_ERR = + GsonJsonAnySetter.class.getSimpleName() + " first parameter must be a string: "; + + /** + * Maps an input property name to an item within the class, where item is one of: + * {@link Field}, {@link Method}, or {@code null}. Entries are overwritten as new + * items are added. + */ + private final Map<String, Object> inProps = new HashMap<>(); + + /** + * Maps an output property name to an item within the class, where item is one of: + * {@link Field}, {@link Method}, or {@code null}. Entries are overwritten as new + * items are added. + */ + private final Map<String, Object> outProps = new HashMap<>(); + + /** + * Maps a method name to a "get" method. Used when overriding properties associated + * with a method. + */ + private final Map<String, Method> getters = new HashMap<>(); + + /** + * Maps a method name to a "set" method. Used when overriding properties associated + * with a method. + */ + private final Map<String, Method> setters = new HashMap<>(); + + /** + * Method having {@link GsonJsonAnyGetter} annotation. Overwritten as new "any-getters" + * are identified. + */ + private Method anyGetter = null; + + /** + * Method having {@link GsonJsonAnySetter} annotation. Overwritten as new "any-setters" + * are identified. + */ + private Method anySetter = null; + + + public Method getAnyGetter() { + return anyGetter; + } + + public Method getAnySetter() { + return anySetter; + } + + /** + * Gets the names of input properties that are not being ignored. + * + * @return the non-ignored input property names + */ + public List<String> getInNotIgnored() { + return getNonNull(inProps); + } + + /** + * Gets the names of output properties that are not being ignored. + * + * @return the non-ignored output property names + */ + public List<String> getOutNotIgnored() { + return getNonNull(outProps); + } + + /** + * Gets the property names, associated with a non-null value, from a set of + * properties. + * + * @param props set of properties from which to extract the names + * @return the property names having a non-null value + */ + private List<String> getNonNull(Map<String, Object> props) { + List<String> lst = new ArrayList<String>(props.size()); + + for (Entry<String, Object> ent : props.entrySet()) { + if (ent.getValue() != null) { + lst.add(ent.getKey()); + } + } + + return lst; + } + + /** + * Gets the input properties whose values are of the given class. + * + * @param clazz class of properties to get + * @return the input properties of the given class + */ + public <T> List<T> getInProps(Class<T> clazz) { + return getProps(clazz, inProps.values()); + } + + /** + * Gets the output properties whose values are of the given class. + * + * @param clazz class of properties to get + * @return the output properties of the given class + */ + public <T> List<T> getOutProps(Class<T> clazz) { + return getProps(clazz, outProps.values()); + } + + /** + * Gets the properties whose values are of the given class. + * + * @param clazz class of properties to get + * @param values values from which to select + * @return the output properties of the given class + */ + @SuppressWarnings("unchecked") + private <T> List<T> getProps(Class<T> clazz, Collection<Object> values) { + List<T> lst = new ArrayList<T>(values.size()); + + for (Object val : values) { + if (val != null && val.getClass() == clazz) { + lst.add((T) val); + } + } + + return lst; + } + + /** + * Recursively walks a class hierarchy, including super classes and interfaces, + * examining each class for various annotations. + * + * @param clazz class whose hierarchy is to be walked + */ + public void walkClassHierarchy(Class<?> clazz) { + if (clazz == Object.class) { + return; + } + + // walk interfaces first + for (Class<?> intfc : clazz.getInterfaces()) { + walkClassHierarchy(intfc); + } + + // walk superclass next, overwriting previous items + Class<?> sup = clazz.getSuperclass(); + if (sup != null) { + walkClassHierarchy(sup); + } + + // finally, examine this class, overwriting previous items + examine(clazz); + } + + /** + * Examines a class for annotations, examining fields and then methods. + * + * @param clazz class to be examined + */ + protected void examine(Class<?> clazz) { + for (Field field : clazz.getDeclaredFields()) { + examine(field); + } + + for (Method method : clazz.getDeclaredMethods()) { + examine(method); + } + } + + /** + * Examines a field for annotations. + * + * @param field field to be examined + */ + protected void examine(Field field) { + if (field.isSynthetic()) { + return; + } + + int mod = field.getModifiers(); + + if (Modifier.isStatic(mod)) { + // skip static fields + return; + } + + if (!Modifier.isPublic(mod) && field.getAnnotation(GsonJsonProperty.class) == null) { + // private/protected - skip it unless explicitly exposed + return; + } + + if (Modifier.isTransient(mod) && field.getAnnotation(GsonJsonProperty.class) == null) { + // transient - skip it unless explicitly exposed + return; + } + + String name = Adapter.detmPropName(field); + if (name == null) { + // invalid name + return; + } + + // if ignoring, then insert null into the map, otherwise insert the field + Field annotField = (field.getAnnotation(GsonJsonIgnore.class) != null ? null : field); + + // a field can be both an input and an output + + inProps.put(name, annotField); + outProps.put(name, annotField); + } + + /** + * Examines a method for annotations. + * + * @param method method to be examined + */ + protected void examine(Method method) { + if (method.isSynthetic()) { + return; + } + + int mod = method.getModifiers(); + + if (Modifier.isStatic(mod)) { + // static methods are not exposed + return; + } + + GsonJsonProperty prop = method.getAnnotation(GsonJsonProperty.class); + GsonJsonAnyGetter get = method.getAnnotation(GsonJsonAnyGetter.class); + GsonJsonAnySetter set = method.getAnnotation(GsonJsonAnySetter.class); + + if (!Modifier.isPublic(mod) && prop == null && get == null && set == null) { + // private/protected methods are not exposed, unless annotated + return; + } + + + if (method.getReturnType() == void.class) { + // "void" return type - must be a "setter" method + if (set == null) { + examineSetter(method); + + } else { + examineAnySetter(method); + } + + } else { + // must be a "getter" method + if (get == null) { + examineGetter(method); + + } else { + examineAnyGetter(method); + } + } + } + + /** + * Examines a "setter" method. + * + * @param method method to be examined + */ + private void examineSetter(Method method) { + String name = Adapter.detmSetterPropName(method); + if (name != null && method.getParameterCount() == 1) { + // remove old name mapping, if any + Method old = setters.get(method.getName()); + if (old != null) { + inProps.remove(Adapter.detmSetterPropName(old)); + } + + setters.put(method.getName(), method); + + // if ignoring, then insert null into the map, otherwise insert the method + inProps.put(name, (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method)); + } + } + + /** + * Examines a "getter" method. + * + * @param method method to be examined + */ + private void examineGetter(Method method) { + String name = Adapter.detmGetterPropName(method); + if (name != null && method.getParameterCount() == 0) { + // remove old name mapping, if any + Method old = getters.get(method.getName()); + if (old != null) { + outProps.remove(Adapter.detmGetterPropName(old)); + } + + getters.put(method.getName(), method); + + // if ignoring, then insert null into the map, otherwise insert the method + outProps.put(name, (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method)); + } + } + + /** + * Examines a method having a {@link GsonJsonAnySetter} annotation. + * + * @param method method to be examined + */ + private void examineAnySetter(Method method) { + if (method.getParameterCount() != 2) { + throw new JsonParseException(ANY_SETTER_MISMATCH_ERR + getFqdn(method)); + } + + if (method.getParameterTypes()[0] != String.class) { + throw new JsonParseException(ANY_SETTER_TYPE_ERR + getFqdn(method)); + } + + // if ignoring, then use null, otherwise use the method + anySetter = (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method); + } + + /** + * Examines a method having a {@link GsonJsonAnyGetter} annotation. + * + * @param method method to be examined + */ + private void examineAnyGetter(Method method) { + if (method.getParameterCount() != 0) { + throw new JsonParseException(ANY_GETTER_MISMATCH_ERR + getFqdn(method)); + } + + // if ignoring, then use null, otherwise use the method + anyGetter = (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method); + } + + /** + * Gets the fully qualified name of a method. + * + * @param method method whose name is desired + * @return the fully qualified method name + */ + private String getFqdn(Method method) { + return (method.getDeclaringClass().getName() + "." + method.getName()); + } +} diff --git a/gson/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java b/gson/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java new file mode 100644 index 00000000..f2975860 --- /dev/null +++ b/gson/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.JsonObject; + +/** + * Super class of all de-serializers. + */ +public interface Deserializer { + + String INVOKE_ERR = "cannot invoke method to deserialize: "; + + /** + * Gets an value from a tree, converts it, and puts it into a target object. + * + * @param source tree from which to get the value + * @param target where to place the converted value + */ + void getFromTree(JsonObject source, Object target); +} diff --git a/gson/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java b/gson/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java new file mode 100644 index 00000000..85ecfea4 --- /dev/null +++ b/gson/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java @@ -0,0 +1,156 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import javax.ws.rs.core.MediaType; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.common.gson.GsonMessageBodyHandler; + +public class GsonMessageBodyHandlerTest { + private static final String GEN_TYPE = "some-type"; + private static final String[] subtypes = {"json", "jSoN", "hello+json", "javascript", "x-javascript", "x-json"}; + + @SuppressWarnings("rawtypes") + private static final Class GEN_CLASS = MyObject.class; + + @SuppressWarnings("unchecked") + private static final Class<Object> CLASS_OBJ = GEN_CLASS; + + private GsonMessageBodyHandler hdlr; + + @Before + public void setUp() { + hdlr = new GsonMessageBodyHandler(); + } + + @Test + public void testIsWriteable() { + // null media type + assertTrue(hdlr.isWriteable(null, null, null, null)); + + for (String subtype : subtypes) { + assertTrue("writeable " + subtype, hdlr.isWriteable(null, null, null, new MediaType(GEN_TYPE, subtype))); + + } + + // the remaining should be FALSE + + // null subtype + assertFalse(hdlr.isWriteable(null, null, null, new MediaType(GEN_TYPE, null))); + + // text subtype + assertFalse(hdlr.isWriteable(null, null, null, MediaType.TEXT_HTML_TYPE)); + } + + @Test + public void testGetSize() { + assertEquals(-1, hdlr.getSize(null, null, null, null, null)); + } + + @Test + public void testWriteTo_testReadFrom() throws Exception { + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + MyObject obj1 = new MyObject(10); + hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr); + + Object obj2 = hdlr.readFrom(CLASS_OBJ, CLASS_OBJ, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(obj1.toString(), obj2.toString()); + } + + @Test + public void testWriteTo_DifferentTypes() throws Exception { + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + + // use a derived type, but specify the base type when writing + MyObject obj1 = new MyObject(10) {}; + hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr); + + Object obj2 = hdlr.readFrom(CLASS_OBJ, CLASS_OBJ, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(obj1.toString(), obj2.toString()); + } + + @Test + public void testIsReadable() { + // null media type + assertTrue(hdlr.isReadable(null, null, null, null)); + + // null subtype + assertFalse(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, null))); + + for (String subtype : subtypes) { + assertTrue("readable " + subtype, hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, subtype))); + + } + + // the remaining should be FALSE + + // null subtype + assertFalse(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, null))); + + // text subtype + assertFalse(hdlr.isReadable(null, null, null, MediaType.TEXT_HTML_TYPE)); + } + + @Test + public void testReadFrom_DifferentTypes() throws Exception { + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + MyObject obj1 = new MyObject(10); + hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr); + + // use a derived type, but specify the base type when reading + @SuppressWarnings("rawtypes") + Class clazz = new MyObject() {}.getClass(); + + @SuppressWarnings("unchecked") + Class<Object> objclazz = clazz; + + Object obj2 = hdlr.readFrom(objclazz, CLASS_OBJ, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(obj1.toString(), obj2.toString()); + } + + public static class MyObject { + private int id; + + public MyObject() { + super(); + } + + public MyObject(int id) { + this.id = id; + } + + @Override + public String toString() { + return "MyObject [id=" + id + "]"; + } + } + +} diff --git a/gson/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java b/gson/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java new file mode 100644 index 00000000..4b5473c5 --- /dev/null +++ b/gson/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java @@ -0,0 +1,203 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import java.lang.reflect.GenericArrayType; +import java.util.LinkedList; +import java.util.TreeMap; +import org.junit.BeforeClass; +import org.junit.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; + +public class JacksonExclusionStrategyTest { + + private static JacksonExclusionStrategy strategy; + private static Gson gson; + + @BeforeClass + public static void setUpBeforeClass() { + strategy = new JacksonExclusionStrategy(); + gson = new GsonBuilder().setExclusionStrategies(strategy).create(); + } + + @Test + public void testWithGson() { + Derived data = new Derived(); + data.setId(10); + data.setText("some text"); + data.setValue("some value"); + + // no fields should be serialized + String result = gson.toJson(data); + assertEquals("{}", result); + + // no fields should be deserialized + result = "{'id':20, 'text':'my text', 'value':'my value'}".replace('\'', '"'); + Derived data2 = gson.fromJson(result, Derived.class); + assertEquals(new Derived().toString(), data2.toString()); + } + + @Test + public void testShouldSkipField() throws Exception { + // should skip every field of Data + assertTrue(strategy.shouldSkipField(new FieldAttributes(Data.class.getDeclaredField("id")))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(Data.class.getDeclaredField("text")))); + + // should not skip fields in Map + assertFalse(strategy.shouldSkipField(new FieldAttributes(MyMap.class.getDeclaredField("mapId")))); + } + + @Test + public void testShouldSkipClass() { + assertFalse(strategy.shouldSkipClass(null)); + assertFalse(strategy.shouldSkipClass(Object.class)); + } + + @Test + public void testIsManaged() { + assertTrue(JacksonExclusionStrategy.isManaged(Data.class)); + assertTrue(JacksonExclusionStrategy.isManaged(Intfc.class)); + assertTrue(JacksonExclusionStrategy.isManaged(com.google.gson.TypeAdapter.class)); + + // generic classes + assertFalse(JacksonExclusionStrategy.isManaged(new Data[0].getClass())); + assertFalse(JacksonExclusionStrategy.isManaged(Enum.class)); + assertFalse(JacksonExclusionStrategy.isManaged(boolean.class)); + assertFalse(JacksonExclusionStrategy.isManaged(byte.class)); + assertFalse(JacksonExclusionStrategy.isManaged(short.class)); + assertFalse(JacksonExclusionStrategy.isManaged(int.class)); + assertFalse(JacksonExclusionStrategy.isManaged(long.class)); + assertFalse(JacksonExclusionStrategy.isManaged(float.class)); + assertFalse(JacksonExclusionStrategy.isManaged(double.class)); + assertFalse(JacksonExclusionStrategy.isManaged(char.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Boolean.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Byte.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Short.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Integer.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Long.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Float.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Double.class)); + assertFalse(JacksonExclusionStrategy.isManaged(Character.class)); + assertFalse(JacksonExclusionStrategy.isManaged(String.class)); + assertFalse(JacksonExclusionStrategy.isManaged(MyMap.class)); + assertFalse(JacksonExclusionStrategy.isManaged(MyList.class)); + assertFalse(JacksonExclusionStrategy.isManaged(MyJson.class)); + assertFalse(JacksonExclusionStrategy.isManaged(GenericArrayType.class)); + } + + /** + * Used to verify that no fields are exposed. + */ + public static class Data { + private int id; + public String text; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + @Override + public String toString() { + return "Data [id=" + id + ", text=" + text + "]"; + } + } + + public static class Derived extends Data { + protected String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "Derived [value=" + value + ", " + super.toString() + "]"; + } + } + + /** + * Used to verify that enums are not managed. + */ + public static enum Enum { + UP, DOWN, + } + + /** + * Used to verify that interfaces <i>are</i> managed. + */ + public static interface Intfc { + int getId(); + } + + /** + * Used to verify that Maps are not managed. + */ + public static class MyMap extends TreeMap<String, Data> { + private static final long serialVersionUID = 1L; + + private int mapId; + + public int getMapId() { + return mapId; + } + } + + /** + * Used to verify that Collections are not managed. + */ + public static class MyList extends LinkedList<Data> { + private static final long serialVersionUID = 1L; + } + + /** + * Used to verify that JsonElements are not managed. + */ + public static class MyJson extends JsonElement { + @Override + public JsonElement deepCopy() { + return null; + } + } +} diff --git a/gson/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java b/gson/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java new file mode 100644 index 00000000..fcb0d9ad --- /dev/null +++ b/gson/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java @@ -0,0 +1,387 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import org.junit.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; +import org.onap.policy.common.gson.internal.Adapter; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; +import org.onap.policy.common.gson.internal.DataAdapterFactory.DerivedData; + +public class AdapterTest { + private static final String GET_VALUE_NAME = "getValue"; + private static final String VALUE_NAME = "value"; + private static final String MY_NAME = AdapterTest.class.getName(); + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + /* + * The remaining fields are just used within the tests. + */ + + private String value; + + // empty alias - should use field name + @GsonJsonProperty("") + protected String emptyAlias; + + @GsonJsonProperty("name-with-alias") + protected String nameWithAlias; + + protected String unaliased; + + protected String $invalidFieldName; + + private List<Data> listField; + + private Data dataField; + + + @Test + public void testIsManagedField() { + assertTrue(Adapter.isManaged(field(VALUE_NAME))); + + assertFalse(Adapter.isManaged(field("$invalidFieldName"))); + } + + @Test + public void testIsManagedMethod() { + assertTrue(Adapter.isManaged(mget(GET_VALUE_NAME))); + + assertFalse(Adapter.isManaged(mget("get$InvalidName"))); + assertFalse(Adapter.isManaged(mset("set$InvalidName"))); + } + + @Test + public void testAdapterField_Converter() { + Adapter adapter = new Adapter(gson, field("dataField")); + + // first, write something of type Data + dataAdapter.reset(); + dataField = new Data(300); + JsonElement tree = adapter.toJsonTree(dataField); + assertEquals("{'id':300}".replace('\'', '"'), tree.toString()); + + // now try a subclass + dataAdapter.reset(); + dataField = new DerivedData(300, "three"); + tree = adapter.toJsonTree(dataField); + assertEquals("{'id':300,'text':'three'}".replace('\'', '"'), tree.toString()); + } + + @Test + @SuppressWarnings("unchecked") + public void testAdapterField_Converter_List() { + listField = DataAdapterFactory.makeList(); + + Adapter adapter = new Adapter(gson, field("listField")); + + dataAdapter.reset(); + JsonElement tree = adapter.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + + // encode it twice so it uses the cached converter + dataAdapter.reset(); + tree = adapter.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + + dataAdapter.reset(); + List<Data> lst2 = (List<Data>) adapter.fromJsonTree(tree); + assertTrue(dataAdapter.isDataRead()); + + assertEquals(listField.toString(), lst2.toString()); + + // decode it twice so it uses the cached converter + dataAdapter.reset(); + lst2 = (List<Data>) adapter.fromJsonTree(tree); + assertTrue(dataAdapter.isDataRead()); + + assertEquals(listField.toString(), lst2.toString()); + } + + @Test + public void testAdapterMethod_Converter() throws Exception { + listField = DataAdapterFactory.makeList(); + + Method getter = mget("getMyList"); + + Adapter aget = new Adapter(gson, getter, true, getter.getReturnType()); + + dataAdapter.reset(); + JsonElement tree = aget.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + + Method setter = AdapterTest.class.getDeclaredMethod("setMyList", List.class); + Adapter aset = new Adapter(gson, setter, true, setter.getGenericParameterTypes()[0]); + + dataAdapter.reset(); + @SuppressWarnings("unchecked") + List<Data> lst2 = (List<Data>) aset.fromJsonTree(tree); + assertTrue(dataAdapter.isDataRead()); + + assertEquals(listField.toString(), lst2.toString()); + } + + @Test + public void testGetPropName_testGetFullName_testMakeError() { + // test field + Adapter adapter = new Adapter(gson, field(VALUE_NAME)); + + assertEquals(VALUE_NAME, adapter.getPropName()); + assertEquals(MY_NAME + ".value", adapter.getFullName()); + + + // test getter + adapter = new Adapter(gson, mget(GET_VALUE_NAME), true, String.class); + + assertEquals(VALUE_NAME, adapter.getPropName()); + assertEquals(MY_NAME + ".getValue", adapter.getFullName()); + + assertEquals("hello: " + MY_NAME + ".getValue", adapter.makeError("hello: ")); + + + // test setter + adapter = new Adapter(gson, mset("setValue"), false, String.class); + + assertEquals(VALUE_NAME, adapter.getPropName()); + assertEquals(MY_NAME + ".setValue", adapter.getFullName()); + } + + @Test + public void testToJsonTree() { + Adapter adapter = new Adapter(gson, field(VALUE_NAME)); + + JsonElement tree = adapter.toJsonTree("hello"); + assertTrue(tree.isJsonPrimitive()); + assertEquals("hello", tree.getAsString()); + } + + @Test + public void testFromJsonTree() { + Adapter adapter = new Adapter(gson, field(VALUE_NAME)); + + assertEquals("world", adapter.fromJsonTree(new JsonPrimitive("world"))); + } + + @Test + public void testDetmPropName() { + assertEquals("emptyAlias", Adapter.detmPropName(field("emptyAlias"))); + assertEquals("name-with-alias", Adapter.detmPropName(field("nameWithAlias"))); + assertEquals("unaliased", Adapter.detmPropName(field("unaliased"))); + assertEquals(null, Adapter.detmPropName(field("$invalidFieldName"))); + } + + @Test + public void testDetmGetterPropName() { + assertEquals("emptyAlias", Adapter.detmGetterPropName(mget("getEmptyAlias"))); + assertEquals("get-with-alias", Adapter.detmGetterPropName(mget("getWithAlias"))); + assertEquals("plain", Adapter.detmGetterPropName(mget("getPlain"))); + assertEquals("primBool", Adapter.detmGetterPropName(mget("isPrimBool"))); + assertEquals("boxedBool", Adapter.detmGetterPropName(mget("isBoxedBool"))); + assertEquals(null, Adapter.detmGetterPropName(mget("isString"))); + assertEquals(null, Adapter.detmGetterPropName(mget("noGet"))); + assertEquals(null, Adapter.detmGetterPropName(mget("get"))); + assertEquals(null, Adapter.detmGetterPropName(mget("get$InvalidName"))); + } + + @Test + public void testDetmSetterPropName() { + assertEquals("emptyAlias", Adapter.detmSetterPropName(mset("setEmptyAlias"))); + assertEquals("set-with-alias", Adapter.detmSetterPropName(mset("setWithAlias"))); + assertEquals("plain", Adapter.detmSetterPropName(mset("setPlain"))); + assertEquals(null, Adapter.detmSetterPropName(mset("noSet"))); + assertEquals(null, Adapter.detmSetterPropName(mset("set"))); + assertEquals(null, Adapter.detmSetterPropName(mset("set$InvalidName"))); + } + + @Test + public void testGetQualifiedNameField() throws Exception { + assertEquals(MY_NAME + ".value", Adapter.getQualifiedName(AdapterTest.class.getDeclaredField(VALUE_NAME))); + } + + @Test + public void testGetQualifiedNameMethod() throws Exception { + assertEquals(MY_NAME + ".getValue", Adapter.getQualifiedName(mget(GET_VALUE_NAME))); + } + + /** + * Gets a field from this class, by name. + * + * @param name name of the field to get + * @return the field + */ + private Field field(String name) { + try { + return AdapterTest.class.getDeclaredField(name); + + } catch (SecurityException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets a "getter" method from this class, by name. + * + * @param name name of the method to get + * @return the method + */ + private Method mget(String name) { + try { + return AdapterTest.class.getDeclaredMethod(name); + + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets a "setter" method from this class, by name. + * + * @param name name of the method to get + * @return the method + */ + private Method mset(String name) { + try { + return AdapterTest.class.getDeclaredMethod(name, String.class); + + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } + + /* + * The remaining methods are just used within the tests. + */ + + protected String getValue() { + return value; + } + + // empty alias - should use method name + @GsonJsonProperty("") + protected String getEmptyAlias() { + return ""; + } + + @GsonJsonProperty("get-with-alias") + protected String getWithAlias() { + return ""; + } + + // no alias, begins with "get" + protected String getPlain() { + return ""; + } + + // begins with "is", returns primitive boolean + protected boolean isPrimBool() { + return true; + } + + // begins with "is", returns boxed Boolean + protected Boolean isBoxedBool() { + return true; + } + + // begins with "is", but doesn't return a boolean + protected String isString() { + return ""; + } + + // doesn't begin with "get" + protected String noGet() { + return ""; + } + + // nothing after "get" + protected String get() { + return ""; + } + + // name has a bogus character + protected String get$InvalidName() { + return ""; + } + + + protected void setValue(String text) { + // do nothing + } + + // empty alias - should use method name + @GsonJsonProperty("") + protected void setEmptyAlias(String text) { + // do nothing + } + + @GsonJsonProperty("set-with-alias") + protected void setWithAlias(String text) { + // do nothing + } + + // no alias, begins with "set" + protected void setPlain(String text) { + // do nothing + } + + // doesn't begin with "set" + protected void noSet(String text) { + // do nothing + } + + // nothing after "get" + protected void set(String text) { + // do nothing + } + + // name has a bogus character + protected void set$InvalidName(String text) { + // do nothing + } + + // returns a list + protected List<Data> getMyList() { + return listField; + } + + // accepts a list + protected void setMyList(List<Data> newList) { + listField = newList; + } +} diff --git a/gson/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java b/gson/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java new file mode 100644 index 00000000..1a15be09 --- /dev/null +++ b/gson/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java @@ -0,0 +1,507 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import com.google.gson.JsonParseException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter; +import org.onap.policy.common.gson.annotation.GsonJsonAnySetter; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; +import org.onap.policy.common.gson.internal.Adapter; +import org.onap.policy.common.gson.internal.ClassWalker; + +public class ClassWalkerTest { + + private MyWalker walker; + + /** + * Set up. + */ + @Before + public void setUp() { + walker = new MyWalker(); + } + + @Test + public void testExamineClassOfQ_testExamineField_testExamineInField_testExamineOutField() { + walker.walkClassHierarchy(DerivedFromBottom.class); + + assertEquals("[Intfc1, Intfc2, Intfc1, Intfc3, Bottom, DerivedFromBottom]", walker.classes.toString()); + + List<String> inFields = walker.getInProps(Field.class).stream().map(field -> field.getName()) + .collect(Collectors.toList()); + Collections.sort(inFields); + assertEquals("[exposedField, overriddenValue, transField]", inFields.toString()); + + List<String> outFields = walker.getInProps(Field.class).stream().map(field -> field.getName()) + .collect(Collectors.toList()); + Collections.sort(outFields); + assertEquals("[exposedField, overriddenValue, transField]", outFields.toString()); + + // should work with interfaces without throwing an NPE + walker.walkClassHierarchy(Intfc1.class); + } + + @Test + public void testHasAnyGetter() { + walker.walkClassHierarchy(Object.class); + assertNull(walker.getAnyGetter()); + assertNull(walker.getAnySetter()); + + walker.walkClassHierarchy(AnyGetterIgnored.class); + assertNull(walker.getAnyGetter()); + assertNull(walker.getAnySetter()); + + walker.walkClassHierarchy(AnyGetterOnly.class); + assertNotNull(walker.getAnyGetter()); + assertNull(walker.getAnySetter()); + } + + @Test + public void testHasAnySetter() { + walker.walkClassHierarchy(Object.class); + assertNull(walker.getAnySetter()); + assertNull(walker.getAnyGetter()); + + walker.walkClassHierarchy(AnySetterIgnored.class); + assertNull(walker.getAnySetter()); + assertNull(walker.getAnyGetter()); + + walker.walkClassHierarchy(AnySetterOnly.class); + assertNotNull(walker.getAnySetter()); + assertNull(walker.getAnyGetter()); + } + + @Test + public void testExamineMethod() { + walker.walkClassHierarchy(DerivedFromData.class); + + assertEquals("[Data, DerivedFromData]", walker.classes.toString()); + + // ensure all methods were examined + Collections.sort(walker.methods); + List<String> lst = Arrays.asList("getId", "getValue", "getOnlyOut", "getStatic", "getText", "getTheMap", + "getUnserialized", "getValue", "getWithParams", "setExtraParams", "setId", "setMap", + "setMapValue", "setMissingParams", "setNonPublic", "setOnlyIn", "setText", "setUnserialized", + "setValue", "setValue", "wrongGetPrefix", "wrongSetPrefix"); + Collections.sort(lst); + assertEquals(lst.toString(), walker.methods.toString()); + + assertNotNull(walker.getAnyGetter()); + assertEquals("getTheMap", walker.getAnyGetter().getName()); + + List<String> getters = walker.getOutProps(Method.class).stream().map(method -> method.getName()) + .collect(Collectors.toList()); + Collections.sort(getters); + assertEquals("[getId, getOnlyOut, getValue]", getters.toString()); + + assertNotNull(walker.getAnySetter()); + assertEquals("setMapValue", walker.getAnySetter().getName()); + + List<String> setters = walker.getInProps(Method.class).stream().map(method -> method.getName()) + .collect(Collectors.toList()); + Collections.sort(setters); + assertEquals("[setId, setOnlyIn, setValue]", setters.toString()); + + // getter with invalid parameter count + assertThatThrownBy(() -> walker.walkClassHierarchy(AnyGetterMismatchParams.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_GETTER_MISMATCH_ERR + + AnyGetterMismatchParams.class.getName() + ".getTheMap"); + + // setter with too few parameters + assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterTooFewParams.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_MISMATCH_ERR + + AnySetterTooFewParams.class.getName() + ".setOverride"); + + // setter with too many parameters + assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterTooManyParams.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_MISMATCH_ERR + + AnySetterTooManyParams.class.getName() + ".setOverride"); + + // setter with invalid parameter type + assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterInvalidParam.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_TYPE_ERR + + AnySetterInvalidParam.class.getName() + ".setOverride"); + } + + @Test + public void testExamineMethod_AnyGetter() { + walker.walkClassHierarchy(AnyGetterOverride.class); + + assertNotNull(walker.getAnyGetter()); + assertEquals("getOverride", walker.getAnyGetter().getName()); + } + + @Test + public void testExamineMethod_AnySetter() { + walker.walkClassHierarchy(AnySetterOverride.class); + + assertNotNull(walker.getAnySetter()); + assertEquals("setOverride", walker.getAnySetter().getName()); + } + + @Test + public void testGetInNotIgnored_testGetOutNotIgnored() { + walker.walkClassHierarchy(DerivedFromData.class); + + assertEquals("[id, onlyIn, text, value]", new TreeSet<>(walker.getInNotIgnored()).toString()); + assertEquals("[id, onlyOut, text, value]", new TreeSet<>(walker.getOutNotIgnored()).toString()); + } + + /** + * Walker subclass that records items that are examined. + */ + private static class MyWalker extends ClassWalker { + private List<String> classes = new ArrayList<>(); + private List<String> methods = new ArrayList<>(); + + @Override + protected void examine(Class<?> clazz) { + classes.add(clazz.getSimpleName()); + + super.examine(clazz); + } + + @Override + protected void examine(Method method) { + if (Adapter.isManaged(method)) { + methods.add(method.getName()); + } + + super.examine(method); + } + } + + protected static interface Intfc1 { + int id = 1000; + } + + protected static interface Intfc2 { + String text = "intfc2-text"; + } + + private static interface Intfc3 { + + } + + protected static class Bottom implements Intfc1, Intfc3 { + private int id; + public String value; + + public String invalid$fieldName; + + @GsonJsonProperty("exposed") + private String exposedField; + + @GsonJsonIgnore + public int ignored; + + public transient int ignoredTransField; + + @GsonJsonProperty("trans") + public transient int transField; + + @GsonJsonIgnore + public int getId() { + return id; + } + + @GsonJsonIgnore + public void setId(int id) { + this.id = id; + } + } + + protected static class DerivedFromBottom extends Bottom implements Intfc1, Intfc2 { + private String text; + protected String anotherValue; + + @GsonJsonProperty("value") + public String overriddenValue; + + @GsonJsonIgnore + public String getText() { + return text; + } + + @GsonJsonIgnore + public void setText(String text) { + this.text = text; + } + } + + protected static class Data { + private int id; + private String text; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + // not public, but property provided + @GsonJsonProperty("text") + protected String getText() { + return text; + } + + // this will be ignored, because there's already a field by this name + public void setText(String text) { + this.text = text; + } + + // should only show up in the output list + public int getOnlyOut() { + return 1100; + } + + // will be overridden by subclass + @GsonJsonProperty("super-value-getter") + public String getValue() { + return null; + } + + // will be overridden by subclass + @GsonJsonProperty("super-value-setter") + public void setValue(String value) { + // do nothing + } + } + + protected static class DerivedFromData extends Data { + // not serialized + private String unserialized; + + // overrides private field and public method from Data + public String text; + + private Map<String, String> map; + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @GsonJsonAnyGetter + public Map<String, String> getTheMap() { + return map; + } + + @GsonJsonIgnore + public void setMap(Map<String, String> map) { + this.map = map; + } + + @GsonJsonAnySetter + public void setMapValue(String key, String value) { + if (map == null) { + map = new TreeMap<>(); + } + + map.put(key, value); + } + + @GsonJsonIgnore + public String getUnserialized() { + return unserialized; + } + + @GsonJsonIgnore + public void setUnserialized(String unserialized) { + this.unserialized = unserialized; + } + + // should only show up in the input list + public void setOnlyIn(int value) { + // do nothing + } + + // has a param - shouldn't be serialized + public int getWithParams(String text) { + return 1000; + } + + // too few params - shouldn't be serialized + public void setMissingParams() { + // do nothing + } + + // too many params - shouldn't be serialized + public void setExtraParams(String text, String moreText) { + // do nothing + } + + // not public - shouldn't be serialized + protected void setNonPublic(String text) { + // do nothing + } + + // doesn't start with "get" + public String wrongGetPrefix() { + return null; + } + + // doesn't start with "set" + public void wrongSetPrefix(String text) { + // do nothing + } + + // static + public static String getStatic() { + return null; + } + } + + /** + * The "get" method has an incorrect argument count. + */ + private static class AnyGetterMismatchParams { + @GsonJsonAnyGetter + public Map<String, String> getTheMap(String arg) { + return new TreeMap<>(); + } + } + + /** + * Has {@link GsonJsonAnyGetter} method. + */ + private static class AnyGetterOnly { + @GsonJsonAnyGetter + private Map<String, Integer> getOverride() { + return null; + } + } + + /** + * Has {@link GsonJsonAnyGetter} method, but it's ignored. + */ + private static class AnyGetterIgnored { + @GsonJsonAnyGetter + @GsonJsonIgnore + private Map<String, Integer> getOverride() { + return null; + } + } + + /** + * Has {@link GsonJsonAnySetter} method. + */ + private static class AnySetterOnly { + @GsonJsonAnySetter + private void setOverride(String key, int value) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnySetter} method, but it's ignored. + */ + private static class AnySetterIgnored { + @GsonJsonAnySetter + @GsonJsonIgnore + private void setOverride(String key, int value) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnyGetter} method that overrides the super class' method. + */ + private static class AnyGetterOverride extends DerivedFromData { + private Map<String, Integer> overMap; + + @GsonJsonAnyGetter + private Map<String, Integer> getOverride() { + return overMap; + } + } + + /** + * Has {@link GsonJsonAnySetter} method that overrides the super class' method. + */ + private static class AnySetterOverride extends DerivedFromData { + private Map<String, Integer> overMap; + + @GsonJsonAnySetter + private void setOverride(String key, int value) { + if (overMap == null) { + overMap = new TreeMap<>(); + } + + overMap.put(key, value); + } + } + + /** + * Has {@link GsonJsonAnySetter} method with too few parameters. + */ + private static class AnySetterTooFewParams extends DerivedFromData { + @GsonJsonAnySetter + public void setOverride(String key) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnySetter} method with too few parameters. + */ + private static class AnySetterTooManyParams extends DerivedFromData { + @GsonJsonAnySetter + public void setOverride(String key, int value, String anotherValue) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnySetter} method whose first argument type is incorrect. + */ + private static class AnySetterInvalidParam extends DerivedFromData { + @GsonJsonAnySetter + public void setOverride(Integer key, String value) { + // do nothing + } + } +} diff --git a/gson/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java b/gson/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java new file mode 100644 index 00000000..d0f0b1ec --- /dev/null +++ b/gson/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java @@ -0,0 +1,310 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Factory used with test Data. + */ +public class DataAdapterFactory implements TypeAdapterFactory { + + /** + * Output of {@link #makeList()}, encoded as json. + */ + public static final String ENCODED_LIST = "[{'id':100},{'id':101}]".replace('\'', '"'); + + /** + * Output of {@link #makeMap()}, encoded as json. + */ + public static final String ENCODED_MAP = "'data-100':{'id':100},'data-101':{'id':101}".replace('\'', '"'); + + /** + * Object handled by this factory. + */ + public static class Data { + private int id; + + public Data() { + super(); + } + + public Data(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String toString() { + return "Data [id=" + id + "]"; + } + } + + /** + * Object derived from Data. + */ + public static class DerivedData extends Data { + private String text; + + public DerivedData() { + super(); + } + + public DerivedData(int id, String text) { + super(id); + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + @Override + public String toString() { + return "DerivedData [text=" + text + ", toString()=" + super.toString() + "]"; + } + } + + /** + * Set to {@code true} when {@link #write(JsonWriter, Data)} has been invoked. + */ + private boolean dataWritten = false; + + /** + * Set to {@code true} when {@link #read(JsonReader)} has been invoked. + */ + private boolean dataRead = false; + + /** + * Clears the flags that indicate that "read" or "write" has been invoked. + */ + public void reset() { + dataWritten = true; + dataRead = true; + } + + public boolean isDataWritten() { + return dataWritten; + } + + public boolean isDataRead() { + return dataRead; + } + + /** + * Makes a list of Data. + * + * @return a new list of Data + */ + public static List<Data> makeList() { + List<Data> listField = new ArrayList<>(); + + listField.add(new Data(100)); + listField.add(new Data(101)); + + return listField; + } + + /** + * Makes an array of Data. + * + * @return a new array of Data + */ + public static JsonArray makeArray() { + JsonArray arr = new JsonArray(); + + for (Data data : makeList()) { + JsonObject json = new JsonObject(); + json.addProperty("id", data.getId()); + arr.add(json); + } + + return arr; + } + + /** + * Makes a map of Data. + * + * @return a new map of Data + */ + public static Map<String, List<Data>> makeMap() { + Map<String, List<Data>> map = new TreeMap<>(); + + for (Data data : makeList()) { + map.put("data-" + data.getId(), Arrays.asList(data)); + } + + return map; + } + + /** + * Adds Data objects to a tree, mirroring {@link #makeMap()}. + * + * @param tree tree into which objects are to be added + */ + public static void addToObject(JsonObject tree) { + for (JsonElement ent : makeArray()) { + JsonObject obj = ent.getAsJsonObject(); + JsonArray arr = new JsonArray(); + arr.add(obj); + tree.add("data-" + obj.get("id").getAsString(), arr); + } + } + + @SuppressWarnings("unchecked") + @Override + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + if (type.getRawType() == Data.class) { + return (TypeAdapter<T>) new DataTypeAdapter(gson.getDelegateAdapter(this, TypeToken.get(Data.class)), + gson.getAdapter(JsonElement.class)); + } + + if (type.getRawType() == DerivedData.class) { + return (TypeAdapter<T>) new DerivedDataTypeAdapter( + gson.getDelegateAdapter(this, TypeToken.get(DerivedData.class)), + gson.getAdapter(JsonElement.class)); + } + + return null; + } + + /** + * Adapter for "Data". + */ + private class DataTypeAdapter extends TypeAdapter<Data> { + private TypeAdapter<Data> delegate; + private TypeAdapter<JsonElement> elementAdapter; + + /** + * Constructs the object. + * + * @param delegate delegate adapter + * @param elementAdapter element adapter + */ + public DataTypeAdapter(TypeAdapter<Data> delegate, TypeAdapter<JsonElement> elementAdapter) { + this.delegate = delegate; + this.elementAdapter = elementAdapter; + } + + @Override + public void write(JsonWriter out, Data data) throws IOException { + dataWritten = true; + + JsonElement tree = delegate.toJsonTree(data); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + jsonObj.addProperty("id", data.getId()); + } + + elementAdapter.write(out, tree); + } + + @Override + public Data read(JsonReader in) throws IOException { + dataRead = true; + + JsonElement tree = elementAdapter.read(in); + Data data = delegate.fromJsonTree(tree); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + data.setId(jsonObj.get("id").getAsInt()); + } + + return data; + } + } + /** + * Adapter for "DerivedData". + */ + private class DerivedDataTypeAdapter extends TypeAdapter<DerivedData> { + private TypeAdapter<DerivedData> delegate; + private TypeAdapter<JsonElement> elementAdapter; + + /** + * Constructs the object. + * + * @param delegate delegate adapter + * @param elementAdapter element adapter + */ + public DerivedDataTypeAdapter(TypeAdapter<DerivedData> delegate, TypeAdapter<JsonElement> elementAdapter) { + this.delegate = delegate; + this.elementAdapter = elementAdapter; + } + + @Override + public void write(JsonWriter out, DerivedData data) throws IOException { + dataWritten = true; + + JsonElement tree = delegate.toJsonTree(data); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + jsonObj.addProperty("id", data.getId()); + jsonObj.addProperty("text", data.getText()); + } + + elementAdapter.write(out, tree); + } + + @Override + public DerivedData read(JsonReader in) throws IOException { + dataRead = true; + + JsonElement tree = elementAdapter.read(in); + DerivedData data = delegate.fromJsonTree(tree); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + data.setId(jsonObj.get("id").getAsInt()); + data.setText(jsonObj.get("text").getAsString()); + } + + return data; + } + } +} |