package org.mapfish.print.attribute; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.Sets; import com.vividsolutions.jts.util.Assert; import com.vividsolutions.jts.util.AssertionFailedException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import org.mapfish.print.ExceptionUtils; import org.mapfish.print.config.Template; import org.mapfish.print.parser.ParserUtils; import org.mapfish.print.wrapper.PArray; import org.mapfish.print.wrapper.PElement; import org.mapfish.print.wrapper.PObject; import org.mapfish.print.wrapper.json.PJsonArray; import org.mapfish.print.wrapper.json.PJsonObject; import org.mapfish.print.wrapper.yaml.PYamlArray; import org.mapfish.print.wrapper.yaml.PYamlObject; import java.lang.reflect.Field; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.annotation.PostConstruct; import static org.mapfish.print.parser.MapfishParser.stringRepresentation; /** * Used for attribute that can have defaults specified in the YAML config file. * * @param <Value> */ public abstract class ReflectiveAttribute<Value> implements Attribute { private static final HashSet<Class<? extends Object>> VALUE_OBJ_FIELD_TYPE_THAT_SHOULD_BE_P_TYPE = createClassSet(PJsonArray.class, PJsonObject.class, JSONObject.class, JSONArray.class); private static final HashSet<Class<? extends Object>> VALUE_OBJ_FIELD_NON_RECURSIVE_TYPE = createClassSet(PElement.class, PArray.class, PObject.class); private static HashSet<Class<? extends Object>> createClassSet(final Object... args) { final HashSet<Class<?>> classes = Sets.newHashSet(); for (Object arg : args) { classes.add((Class<?>) arg); } return classes; } /** * Name of attribute in the client config json. * * @see #printClientConfig(org.json.JSONWriter, org.mapfish.print.config.Template) */ public static final String JSON_NAME = "name"; /** * Name of the required parameters object in the client config json. * * @see #printClientConfig(org.json.JSONWriter, org.mapfish.print.config.Template) */ public static final String JSON_CLIENT_PARAMS = "clientParams"; /** * Name of the value suggestions object in the client config json. * * @see #printClientConfig(org.json.JSONWriter, org.mapfish.print.config.Template) */ public static final String JSON_CLIENT_INFO = "clientInfo"; /** * A string describing the type of the attribute param in the clientConfig. */ public static final String JSON_ATTRIBUTE_TYPE = "type"; /** * If the parameter in the value object is another value object (and not a PObject or PArray) then * this will be a json object describing the embedded param in the same way as each object in clientParams. */ public static final String JSON_ATTRIBUTE_EMBEDDED_TYPE = "embeddedType"; /** * The default value of the attribute param in the optional params. */ public static final String JSON_ATTRIBUTE_DEFAULT = "default"; /** * Json field that declares if the param is an array. */ public static final String JSON_ATTRIBUTE_IS_ARRAY = "isArray"; private PYamlObject defaults; private String configName; private void validateParamObject(final Class<?> typeToTest, final Set<Class> tested) { if (!tested.contains(typeToTest)) { final Collection<Field> allAttributes = ParserUtils.getAllAttributes(typeToTest); Assert.isTrue(!allAttributes.isEmpty(), "An attribute value object must have at least on public field."); for (Field attribute : allAttributes) { Class<?> type = attribute.getType(); if (type.isArray()) { type = type.getComponentType(); } if (VALUE_OBJ_FIELD_NON_RECURSIVE_TYPE.contains(type) || isJavaType(type)) { continue; } if (VALUE_OBJ_FIELD_TYPE_THAT_SHOULD_BE_P_TYPE.contains(type)) { throw new AssertionFailedException(typeToTest.getName() + "#" + attribute.getName() + " should not be a field in a" + " value object. Instead use the more general " + PArray.class.getName() + " or " + PObject.class.getName()); } tested.add(type); validateParamObject(type, tested); } } } private boolean isJavaType(final Class<?> type) { return type.getPackage() == null || type.getPackage().getName().startsWith("java."); } @VisibleForTesting @PostConstruct final void init() { if (this.defaults == null) { this.defaults = new PYamlObject(Collections.<String, Object>emptyMap(), getAttributeName()); } validateParamObject(getValueType(), Sets.<Class>newHashSet()); } /** * Return the type created by {@link #createValue(org.mapfish.print.config.Template)}. */ public abstract Class<? extends Value> getValueType(); /** * The YAML config default values. * * @return the default values */ public final PObject getDefaultValue() { return this.defaults; } /** * <p>Default values for this attribute. Example:</p> * <pre><code> * attributes: * legend: !legend * default: * name: "Legend"</code></pre> * @param defaultValue The default values. */ public final void setDefault(final Map<String, Object> defaultValue) { this.defaults = new PYamlObject(defaultValue, getAttributeName()); } @Override public final void setConfigName(final String configName) { this.configName = configName; } /** * Return a descriptive name of this attribute. */ protected final String getAttributeName() { return getClass().getSimpleName().substring(0, 1).toLowerCase() + getClass().getSimpleName().substring(1); } /** * Create an instance of a attribute value object. Each instance must be new and unique. Instances must <em>NOT</em> be shared. * <p></p> * The object will be populated from the json. Each public field will be populated by looking up the value in the json. * <p></p> * If a field in the object has the {@link org.mapfish.print.parser.HasDefaultValue} annotation then no exception * will be thrown if the json does not contain a value. * <p></p> * Fields in the object with the {@link org.mapfish.print.parser.OneOf} annotation must have one of the fields in the * request data. * <p></p> * <ul> * <li>{@link java.lang.String}</li> * <li>{@link java.lang.Integer}</li> * <li>{@link java.lang.Float}</li> * <li>{@link java.lang.Double}</li> * <li>{@link java.lang.Short}</li> * <li>{@link java.lang.Boolean}</li> * <li>{@link java.lang.Character}</li> * <li>{@link java.lang.Byte}</li> * <li>{@link java.lang.Enum}</li> * <li>PJsonObject</li> * <li>URL</li> * <li>Any enum</li> * <li>PJsonArray</li> * <li>any type with a 0 argument constructor</li> * <li>array of any of the above (String[], boolean[], PJsonObject[], ...)</li> * </ul> * <p></p> * If there is a public <code>{@value org.mapfish.print.parser.MapfishParser#POST_CONSTRUCT_METHOD_NAME}()</code> * method then it will be called after the fields are all set. * <p></p> * In the case where the a parameter type is a normal POJO (not a special case like PJsonObject, URL, enum, double, etc...) * then it will be assumed that the json data is a json object and the parameters will be recursively parsed into the new * object as if it is also MapLayer parameter object. * <p></p> * It is important to put values in the value object as public fields because reflection is used when printing client config * as well as generating documentation. If a field is intended for the client software as information but is not intended * to be set (or sent as part of the request data), the field can be a final field. * * @param template the template that this attribute is part of. */ public abstract Value createValue(Template template); /** * Uses reflection on the object created by {@link #createValue(org.mapfish.print.config.Template)} to create the options. * <p></p> * The public final fields are written as the field name as the key and the value as the value. * <p></p> * The public (non-final) mandatory fields are written as part of clientParams and are written with the field name as the key and * the field type as the value. * <p></p> * The public (non-final) {@link org.mapfish.print.parser.HasDefaultValue} fields are written as part of clientOptions and are * written with the field name as the key and an object as a value with a type property with the type and a default property * containing the default value. * * @param json the json writer to write to * @param template the template that this attribute is part of * @throws org.json.JSONException */ @Override public final void printClientConfig(final JSONWriter json, final Template template) throws JSONException { try { Set<Class> printed = Sets.newHashSet(); final Value exampleValue = createValue(template); json.key(JSON_NAME).value(this.configName); json.key(JSON_ATTRIBUTE_TYPE).value(getValueType().getSimpleName()); final Class<?> valueType = exampleValue.getClass(); json.key(JSON_CLIENT_PARAMS); json.object(); printClientConfigForType(json, exampleValue, valueType, this.defaults, printed); json.endObject(); Optional<JSONObject> clientOptions = getClientInfo(); if (clientOptions.isPresent()) { json.key(JSON_CLIENT_INFO).value(clientOptions.get()); } } catch (Throwable e) { // Note: If this test fails and you just added a new attribute, make // sure to set defaults in AbstractMapfishSpringTest.configureAttributeForTesting throw new Error("Error printing the clientConfig of: " + getValueType().getName(), e); } } /** * Return an object that will be added to the client config with the key <em>clientInfo</em>. */ protected Optional<JSONObject> getClientInfo() throws JSONException { return Optional.absent(); } private void printClientConfigForType(final JSONWriter json, final Object exampleValue, final Class<?> valueType, final PObject defaultValue, final Set<Class> printed) throws JSONException, IllegalAccessException { final Collection<Field> mutableFields = ParserUtils.getAttributes(valueType, ParserUtils.FILTER_ONLY_REQUIRED_ATTRIBUTES); if (!mutableFields.isEmpty()) { for (Field attribute : mutableFields) { encodeAttributeValue(true, json, exampleValue, getDefaultValue(defaultValue, attribute), attribute, printed); } } final Collection<Field> hasDefaultFields = ParserUtils.getAttributes(valueType, ParserUtils.FILTER_HAS_DEFAULT_ATTRIBUTES); if (!hasDefaultFields.isEmpty()) { for (Field attribute : hasDefaultFields) { encodeAttributeValue(false, json, exampleValue, getDefaultValue(defaultValue, attribute), attribute, printed); } } } private Object getDefaultValue(final PObject defaultValue, final Field attribute) { if (defaultValue == null) { return null; } return defaultValue.opt(attribute.getName()); } private void encodeAttributeValue(final boolean required, final JSONWriter json, final Object exampleValue, final Object defaultValue, final Field attribute, final Set<Class> printed) throws JSONException, IllegalAccessException { json.key(attribute.getName()); json.object(); final Class<?> type = attribute.getType(); final Class<?> typeOrComponentType = type.isArray() ? type.getComponentType() : type; if (!VALUE_OBJ_FIELD_NON_RECURSIVE_TYPE.contains(typeOrComponentType) && !isJavaType(typeOrComponentType)) { if (printed.contains(typeOrComponentType)) { json.key(JSON_ATTRIBUTE_TYPE).value("recursiveDefinition"); } else { Set<Class> printedForSubTree = Sets.newHashSet(printed); printedForSubTree.add(typeOrComponentType); json.key(JSON_ATTRIBUTE_TYPE).value(stringRepresentation(type)); json.key(JSON_ATTRIBUTE_EMBEDDED_TYPE); json.object(); Object value = attribute.get(exampleValue); if (value == null) { if (typeOrComponentType.isEnum()) { if (typeOrComponentType.getEnumConstants().length > 0) { value = typeOrComponentType.getEnumConstants()[0]; } } else { try { value = typeOrComponentType.newInstance(); } catch (InstantiationException e) { throw ExceptionUtils.getRuntimeException(e); } } } final Object childDefaultValue = getDefaultValue(((PObject) defaultValue), attribute); printClientConfigForType(json, value, typeOrComponentType, (PObject) childDefaultValue, printedForSubTree); json.endObject(); } } else { final String typeDescription = getTypeDescription(typeOrComponentType); json.key(JSON_ATTRIBUTE_TYPE).value(typeDescription); } if (!required || defaultValue != null) { json.key(JSON_ATTRIBUTE_DEFAULT); Object valueToAdd = defaultValue; if (defaultValue == null) { valueToAdd = attribute.get(exampleValue); } if (valueToAdd instanceof PJsonArray) { valueToAdd = ((PJsonArray) valueToAdd).getInternalArray(); } else if (valueToAdd instanceof PJsonObject) { valueToAdd = ((PJsonObject) valueToAdd).getInternalObj(); } else if (valueToAdd instanceof PYamlObject) { valueToAdd = ((PYamlObject) valueToAdd).toJSON().getInternalObj(); } else if (valueToAdd instanceof PYamlArray) { valueToAdd = ((PYamlArray) valueToAdd).toJSON().getInternalArray(); } json.value(valueToAdd); } if (type.isArray()) { json.key(JSON_ATTRIBUTE_IS_ARRAY).value(type.isArray()); } json.endObject(); } private String getTypeDescription(final Class<?> type) { final String typeDescription; if (PArray.class.isAssignableFrom(type)) { typeDescription = "array"; } else if (PObject.class.isAssignableFrom(type)) { typeDescription = "object"; } else if (Double.class.isAssignableFrom(type)) { typeDescription = "double"; } else if (Integer.class.isAssignableFrom(type)) { typeDescription = "int"; } else if (Boolean.class.isAssignableFrom(type)) { typeDescription = "boolean"; } else if (Byte.class.isAssignableFrom(type)) { typeDescription = "byte"; } else if (Long.class.isAssignableFrom(type)) { typeDescription = "long"; } else if (Float.class.isAssignableFrom(type)) { typeDescription = "float"; } else { typeDescription = stringRepresentation(type); } return typeDescription; } }