/*
* Copyright 2015 Stripes Framework.
*
* 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.
*/
package net.sourceforge.stripes.action;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import net.sourceforge.stripes.util.Log;
/**
* This abstract class should be implemented by "builders" which take Java
* objects and convert them into a specific format (such as JavaScript, XML, or
* JSON). Originally created by using the JavaScript builder which contains
* functionality for property and class exclusion.
*
* @author Rick Grashel
* @param <T> Subclass of this object
*/
public abstract class ObjectOutputBuilder< T extends ObjectOutputBuilder> {
/**
* Log instance used to log messages.
*/
private static final Log log = Log.getInstance(ObjectOutputBuilder.class);
/**
* Holds the set of classes representing the primitive types in Java.
*/
static final Set<Class<?>> simpleTypes = new HashSet<Class<?>>();
/**
* Holds the set of types that will be skipped over by default.
*/
static final Set<Class<?>> ignoredTypes = new HashSet<Class<?>>();
static {
simpleTypes.add(Byte.TYPE);
simpleTypes.add(Short.TYPE);
simpleTypes.add(Integer.TYPE);
simpleTypes.add(Long.TYPE);
simpleTypes.add(Float.TYPE);
simpleTypes.add(Double.TYPE);
simpleTypes.add(Boolean.TYPE);
simpleTypes.add(Character.TYPE);
ignoredTypes.add(Class.class);
}
/**
* Holds the root object which is to be converted to an output format.
*/
private final Object rootObject;
/**
* Holds the (potentially empty) set of user classes that should be skipped
* over.
*/
private final Set<Class<?>> excludeClasses;
/**
* Holds the (potentially empty) set of properties that should be skipped
* over.
*/
private final Set<String> excludeProperties;
/**
* Holds an optional user-supplied name for the root property.
*/
private String rootVariableName = null;
/**
* Constructs a new ObjectOutputBuilder to build output for the root object
* supplied.
*
* @param root The root object from which to being translation into
* JavaScript
* @param objectsToExclude Zero or more Strings and/or Classes to be
* excluded from translation.
*/
public ObjectOutputBuilder(Object root, Object... objectsToExclude) {
this.rootObject = root;
this.excludeClasses = new HashSet<Class<?>>();
this.excludeProperties = new HashSet<String>();
for (Object object : objectsToExclude) {
if (object instanceof Class<?>) {
addClassExclusion((Class<?>) object);
} else if (object instanceof String) {
addPropertyExclusion((String) object);
} else {
log.warn("Don't know to determine exclusion for objects of type ", object.getClass().getName(), ". You may only pass in instances of Class and/or String.");
}
}
this.excludeClasses.addAll(ignoredTypes);
}
/**
* Adds one or more properties to the list of property to exclude when
* translating to the formatted output.
*
* @param property one or more property names to be excluded
* @return the ObjectOutputBuilder instance to simplify method chaining
*/
public final T addPropertyExclusion(String... property) {
this.excludeProperties.addAll(Arrays.asList(property));
return (T) this;
}
/**
* Adds one or more properties to the list of properties to exclude when
* translating to JavaScript.
*
* @param clazz one or more classes to exclude
* @return the JavaScripBuilder instance to simplify method chaining
*/
public final T addClassExclusion(Class<?>... clazz) {
this.excludeClasses.addAll(Arrays.asList(clazz));
return (T) this;
}
/**
* Sets an optional user-supplied root variable name. If set this name will
* be used by the building when declaring the root variable to which the JS
* is assigned. If not provided then a randomly generated name will be used.
*
* @param rootVariableName the name to use when declaring the root variable
*/
public void setRootVariableName(final String rootVariableName) {
this.rootVariableName = rootVariableName;
}
/**
* Returns the name used to declare the root variable to which the built
* object is assigned.
*
* @return The root variable name for the object which is being converted
*/
public String getRootVariableName() {
return rootVariableName;
}
/**
* Causes the JavaScriptBuilder to navigate the properties of the supplied
* object and convert them to JavaScript.
*
* @return String a fragment of JavaScript that will define and return the
* JavaScript equivalent of the Java object supplied to the builder.
* @throws java.lang.Exception
*/
public String build() throws Exception {
Writer writer = new StringWriter();
build(writer);
return writer.toString();
}
/**
* Causes the ObjectOutputBuilder to navigate the properties of the supplied
* object and convert them to the desired format, writing them to the
* supplied writer as it goes.
*
* @param writer Instance of the writer that the converted object should be
* written to.
* @throws java.lang.Exception
*/
public abstract void build(Writer writer) throws Exception;
/**
* Returns true if the supplied type should be excluded from conversion,
* otherwise returns false. A class should be excluded if it is assignable
* to one of the types listed for exclusion, or, it is an array of such a
* type.
*
* @param type - Class to check to see if it is excluded for this builder.
* @return Whether or not the passed class is targeted for exclusion by the
* builder.
*/
public boolean isExcludedType(Class<?> type) {
for (Class<?> excludedType : this.excludeClasses) {
if (excludedType.isAssignableFrom(type)) {
return true;
} else if (type.isArray() && excludedType.isAssignableFrom(type.getComponentType())) {
return true;
}
}
return false;
}
/**
* Returns true if the object is of a type that can be converted to a simple
* scalar, and false otherwise.
*
* @param in Object to check to see if it is a scalar
* @return Whether or not the passed object is a scalar object.
*/
public boolean isScalarType(Object in) {
if (in == null) {
return true; // Though not strictly scalar, null can be treated as such
}
Class<?> type = in.getClass();
return simpleTypes.contains(type)
|| Number.class.isAssignableFrom(type)
|| String.class.isAssignableFrom(type)
|| Boolean.class.isAssignableFrom(type)
|| Character.class.isAssignableFrom(type)
|| Date.class.isAssignableFrom(type);
}
/**
* Returns the root object being built.
*
* @return Root object being built.
*/
public Object getRootObject() {
return this.rootObject;
}
/**
* Returns the properties that should be excluded from this
* object out.
*
* @return Set of excluded properties
*/
public Set<String> getExcludedProperties() {
return this.excludeProperties;
}
}