Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
* 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
*
* 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.
*/
public class WriteOptionsBuilder {
// Constants
public static final String ISO_DATE_FORMAT = "yyyy-MM-dd";
public static final String ISO_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
private static final Map BASE_ALIAS_MAPPINGS = new ConcurrentHashMap<>();
private static final Map, JsonWriter.JsonClassWriter> BASE_WRITERS = new ConcurrentHashMap<>();
private static final Set> BASE_NON_REFS = ConcurrentHashMap.newKeySet();
static final Map, Set> BASE_EXCLUDED_FIELD_NAMES = new ConcurrentHashMap<>();
private static final Map, Map> BASE_NONSTANDARD_GETTERS = new ConcurrentHashMap<>();
private static final Map BASE_FIELD_FILTERS = new ConcurrentHashMap<>();
private static final Map BASE_METHOD_FILTERS = new ConcurrentHashMap<>();
private static final Map BASE_ACCESSOR_FACTORIES = new ConcurrentHashMap<>();
private static final WriteOptions defWriteOptions;
private final DefaultWriteOptions options;
static {
ReadOptionsBuilder.loadBaseAliasMappings(WriteOptionsBuilder::addPermanentAlias);
loadBaseWriters();
loadBaseNonRefs();
loadBaseExcludedFields();
loadBaseNonStandardGetters();
// If the lists below become large, then break these out into load* APIs like we've done for the ones above.
addPermanentFieldFilter("static", new StaticFieldFilter());
addPermanentFieldFilter("enum", new EnumFieldFilter());
addPermanentMethodFilter("default", new DefaultMethodFilter());
addPermanentAccessorFactory("get", new GetMethodAccessorFactory());
addPermanentAccessorFactory("is", new IsMethodAccessorFactory());
defWriteOptions = new WriteOptionsBuilder().build();
// low-level alias support (bigint, bigdec, class, date, and string)
// class, date, and string are already within ClassUtilities.
ClassUtilities.addPermanentClassAlias(BigInteger.class, "bigint");
ClassUtilities.addPermanentClassAlias(BigInteger.class, "BigInt");
ClassUtilities.addPermanentClassAlias(BigDecimal.class, "bigdec");
ClassUtilities.addPermanentClassAlias(BigDecimal.class, "BigDec");
ClassUtilities.addPermanentClassAlias(BigDecimal.class, "Class");
ClassUtilities.addPermanentClassAlias(BigDecimal.class, "String");
ClassUtilities.addPermanentClassAlias(BigDecimal.class, "Date");
}
/**
* @return Default WriteOptions - no construction needed, unmodifiable.
*/
public static WriteOptions getDefaultWriteOptions() {
return defWriteOptions;
}
/**
* Start with default options
*/
public WriteOptionsBuilder() {
options = new DefaultWriteOptions();
// Start with all BASE_ALIAS_MAPPINGS (more aliases can be added to this instance, and more aliases
// can be added to the BASE_ALIAS_MAPPINGS via the static method, so that all instances get them.)
options.nonStandardGetters.putAll(BASE_NONSTANDARD_GETTERS);
options.aliasTypeNames.putAll(BASE_ALIAS_MAPPINGS);
options.customWrittenClasses.putAll(BASE_WRITERS);
options.nonRefClasses.addAll(BASE_NON_REFS);
options.excludedFieldNames.putAll(BASE_EXCLUDED_FIELD_NAMES);
options.fieldFilters.putAll(BASE_FIELD_FILTERS);
options.methodFilters.putAll(BASE_METHOD_FILTERS);
options.accessorFactories.putAll(BASE_ACCESSOR_FACTORIES);
}
/**
* Copy another WriteOptions as a starting point. If null is passed in, then you get the default options.
*/
public WriteOptionsBuilder(WriteOptions copy) {
this();
if (copy != null) {
DefaultWriteOptions other = (DefaultWriteOptions) copy;
// Copy simple settings
options.allowNanAndInfinity = other.allowNanAndInfinity;
options.closeStream = other.closeStream;
options.classLoader = other.classLoader;
options.enumPublicFieldsOnly = other.enumPublicFieldsOnly;
options.forceMapOutputAsTwoArrays = other.forceMapOutputAsTwoArrays;
options.prettyPrint = other.prettyPrint;
options.lruSize = other.lruSize;
options.shortMetaKeys = other.shortMetaKeys;
options.showTypeInfo = other.showTypeInfo;
options.skipNullFields = other.skipNullFields;
options.writeLongsAsStrings = other.writeLongsAsStrings;
// Copy complex settings
options.includedFieldNames.clear();
options.includedFieldNames.putAll(other.includedFieldNames);
options.nonStandardGetters.clear();
options.nonStandardGetters.putAll(other.nonStandardGetters);
options.aliasTypeNames.clear();
options.aliasTypeNames.putAll(other.aliasTypeNames);
options.excludedFieldNames.clear();
options.excludedFieldNames.putAll(other.excludedFieldNames);
options.customWrittenClasses.clear();
options.customWrittenClasses.putAll(other.customWrittenClasses);
options.notCustomWrittenClasses.clear();
options.notCustomWrittenClasses.addAll(other.notCustomWrittenClasses);
options.nonRefClasses.clear();
options.nonRefClasses.addAll(other.nonRefClasses);
options.fieldFilters.clear();
options.fieldFilters.putAll(other.fieldFilters);
options.methodFilters.clear();
options.methodFilters.putAll(other.methodFilters);
options.accessorFactories.clear();
options.accessorFactories.putAll(other.accessorFactories);
// Copy caches
options.accessorsCache = new LRUCache<>(other.lruSize);
options.accessorsCache.putAll(other.accessorsCache);
options.classMetaCache = new LRUCache<>(other.lruSize);
options.classMetaCache.putAll(other.classMetaCache);
}
}
/**
* Call this method to add a permanent (JVM lifetime) alias of a class to an often shorter, name. All WriteOptions
* will automatically be created with any permanent aliases added to them.
*
* @param clazz Class that will be aliased by a shorter name in the JSON.
* @param alias Shorter alias name, for example, "ArrayList" as opposed to "java.util.ArrayList"
*/
public static void addPermanentAlias(Class> clazz, String alias) {
BASE_ALIAS_MAPPINGS.put(clazz.getName(), alias);
}
/**
* Call this method to remove alias patterns from the WriteOptionsBuilder so that when it writes JSON,
* the classes that match the passed in pattern are not aliased. This API matches your wildcard pattern
* of *, ?, and characters against class names in its cache. It removes the entries (className to aliasName)
* from the cache to prevent the resultant classes from being aliased during output.
*
* @param classNamePattern String pattern to match class names. This String matches using a wild-card
* pattern, where * matches anything and ? matches one character. As many * or ? can be used as needed.
*/
public static void removePermanentAliasTypeNamesMatching(String classNamePattern) {
String regex = StringUtilities.wildcardToRegexString(classNamePattern);
Pattern pattern = Pattern.compile(regex);
BASE_ALIAS_MAPPINGS.keySet().removeIf(key -> pattern.matcher(key).matches());
}
/**
* Call this method to add a permanent (JVM lifetime) excluded field name of class. All WriteOptions will
* automatically be created this field field on the excluded list.
*
* @param clazz Class that contains the named field.
* @param fieldName to be excluded.
*/
public static void addPermanentExcludedField(Class> clazz, String fieldName) {
BASE_EXCLUDED_FIELD_NAMES.computeIfAbsent(clazz, cls -> ConcurrentHashMap.newKeySet()).add(fieldName);
}
/**
* Call this method to add a permanent (JVM lifetime) class that should not be treated as referencable
* when being written out to JSON. This means it will never have an @id nor @ref. This feature is
* useful for small, immutable classes.
*
* @param clazz Class that will no longer be treated as referenceable when being written to JSON.
*/
public static void addPermanentNonRef(Class> clazz) {
BASE_NON_REFS.add(clazz);
}
/**
* Call this method to add a custom JSON writer to json-io. It will
* associate the Class 'c' to the writer you pass in. The writers are
* found with isAssignableFrom(). If this is too broad, causing too
* many classes to be associated to the custom writer, you can indicate
* that json-io should not use a custom write for a particular class,
* by calling the addNotCustomWrittenClass() method. This method will add
* the custom writer such that it will be there permanently, for the
* life of the JVM (static).
*
* @param clazz Class to assign a custom JSON writer to
* @param writer The JsonClassWriter which will write the custom JSON format of class.
*/
public static void addPermanentWriter(Class> clazz, JsonWriter.JsonClassWriter writer) {
BASE_WRITERS.put(clazz, writer);
}
/**
* This option permits adding non-standard getters (used when writing JSON) that access properties from objects,
* where the method name does not follow a standard setter/getter property name. For example, on java.time.Instance,
* to get the 'second' field, the accessor method is 'getEpochSecond()'.
* Anything added here will automatically be made in all WriteOptions.
* @param clazz Class that has the non-standard getter. java.time.Instance in the example above.
* @param field String name of the class property. 'second' in the example above.
* @param methodName The name of the non-standard method used to get the field value. 'getEpochSecond' in the example above.
*/
public static void addPermanentNonStandardGetter(Class> clazz, String field, String methodName) {
BASE_NONSTANDARD_GETTERS.computeIfAbsent(clazz, cls -> new ConcurrentHashMap<>()).put(field, methodName);
}
/**
* Add a FieldFilter that is JVM lifecycle scoped.
* @param fieldFilter {@link FieldFilter} class used to eliminate fields from being included in the serialized JSON.
*/
public static void addPermanentFieldFilter(String name, FieldFilter fieldFilter) {
BASE_FIELD_FILTERS.put(name, fieldFilter);
}
/**
* Add a MethodFilter that is JVM lifecycle scoped. All WriteOptions instances will contain this filter.
* @param name String name of this particular MethodFilter instance.
* @param methodFilter {@link MethodFilter} class used to eliminate a particular accessor method from being used.
*/
public static void addPermanentMethodFilter(String name, MethodFilter methodFilter) {
BASE_METHOD_FILTERS.put(name, methodFilter);
}
/**
* Add a MethodFilter that is JVM lifecycle scoped. All WriteOptions instances will contain this filter.
* @param name String name of this particular method filter instance.
* @param clazz class that contains the method to be filtered (can be derived class, with field defined on parent class).
* @param methodName String name of no-argument method to be filtered.
*/
public static void addPermanentNamedMethodFilter(String name, Class> clazz, String methodName) {
BASE_METHOD_FILTERS.put(name, new NamedMethodFilter(clazz, methodName));
}
/**
* Add an AccessorFactory that is JVM lifecycle scoped. All WriteOptions instances will contain this AccessorFactory.
* It is the job of an AccessorFactory to provide a possible method name for a particular field. json-io ships with
* a GetMethodAccessorFactory and an IsMethodAccessFactory. These produce a possible method name for a given field.
* When a field on a Java class is being accessed (read), and it cannot be obtained directly, then all AccessoryFactory
* instances will be consulted until an API can be used to read the field.
* @param name String name of AccessorFactory. Each factory should have it's own unique name. This will allow these
* to be defined in a file, later if needed.
* @param factory AccessorFactory subclass.
*/
public static void addPermanentAccessorFactory(String name, AccessorFactory factory) {
BASE_ACCESSOR_FACTORIES.put(name, factory);
}
/**
* @param loader ClassLoader to use when writing JSON to resolve String named classes.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder classLoader(ClassLoader loader) {
options.classLoader = loader;
return this;
}
/**
* @param shortMetaKeys boolean true to turn on short meta-keys, false for long.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder shortMetaKeys(boolean shortMetaKeys) {
options.shortMetaKeys = shortMetaKeys;
return this;
}
/**
* @param aliases Map containing String class names to alias names. The passed in Map will
* be copied, and be the new baseline settings.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder aliasTypeNames(Map aliases) {
aliases.forEach(this::addUniqueAlias);
return this;
}
/**
* @param type Class to alias
* @param alias String shorter name to use, typically.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder aliasTypeName(Class> type, String alias) {
options.aliasTypeNames.put(type.getName(), alias);
return this;
}
/**
* @param typeName String class name
* @param alias String shorter name to use, typically.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder aliasTypeName(String typeName, String alias) {
addUniqueAlias(typeName, alias);
return this;
}
/**
* Since we are swapping keys/values, we must check for duplicate values (which are now keys).
* @param typeName String fully qualified class name.
* @param alias String shorter alias name.
*/
private void addUniqueAlias(String typeName, String alias) {
Convention.throwIfClassNotFound(typeName, options.classLoader);
Convention.throwIfKeyExists(options.aliasTypeNames, typeName, "Tried to create @type alias '" + alias + "' for '" + typeName + "', but it is already aliased to: " + options.aliasTypeNames.get(typeName));
options.aliasTypeNames.put(typeName, alias);
}
/**
* Remove alias entries from this WriteOptionsBuilder instance where the Java fully qualified string
* class name matches the passed in wildcard pattern.
* @param typeNamePattern String pattern to match class names. This String matches using a wild-card
* pattern, where * matches anything and ? matches one character. As many * or ? can be used as needed.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder removeAliasTypeNamesMatching(String typeNamePattern) {
String regex = StringUtilities.wildcardToRegexString(typeNamePattern);
Pattern pattern = Pattern.compile(regex);
options.aliasTypeNames.keySet().removeIf(key -> pattern.matcher(key).matches());
return this;
}
/**
* Set to always show type
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder showTypeInfoAlways() {
options.showTypeInfo = WriteOptions.ShowType.ALWAYS;
return this;
}
/**
* Set to never show type
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder showTypeInfoNever() {
options.showTypeInfo = WriteOptions.ShowType.NEVER;
return this;
}
/**
* Set to show minimal type. This means that when the type of object can be inferred, a type field will not
* be output. A Field that points to an instance of the same time, or a typed [] of objects don't need the type
* info. However, an Object[], a Collection with no generics, the reader will need to know what type the JSON
* object is, in order to instantiate the write Java class to which the information will be copied.
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder showTypeInfoMinimal() {
options.showTypeInfo = WriteOptions.ShowType.MINIMAL;
return this;
}
/**
* @param prettyPrint boolean 'prettyPrint' setting, true to turn on, false will turn off.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder prettyPrint(boolean prettyPrint) {
options.prettyPrint = prettyPrint;
return this;
}
/**
* @param size Max size of the cache for Class fields and Class accessors
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder lruSize(int size) {
options.lruSize = size;
Map, List> accessorCacheCopy = options.accessorsCache;
options.accessorsCache = new LRUCache<>(options.getLruSize());
options.accessorsCache.putAll(accessorCacheCopy);
Map, Map> classMetaCacheCopy = options.classMetaCache;
options.classMetaCache = new LRUCache<>(options.getLruSize());
options.classMetaCache.putAll(classMetaCacheCopy);
return this;
}
/**
* @param writeLongsAsStrings boolean true to turn on writing longs as Strings, false to write them as
* native JSON longs.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder writeLongsAsStrings(boolean writeLongsAsStrings) {
options.writeLongsAsStrings = writeLongsAsStrings;
return this;
}
/**
* @param skipNullFields boolean setting, where true indicates fields with null values will not be written
* to the JSON, false will allow the field to still be written.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder skipNullFields(boolean skipNullFields) {
options.skipNullFields = skipNullFields;
return this;
}
/**
* @param forceMapOutputAsTwoArrays boolean 'forceMapOutputAsTwoArrays' setting. true will force Java Maps to be
* written out as two parallel arrays, once for keys, one array for values.
* false will write out one JSON { } object, if all keys are Strings. If not,
* then the Map will be output as two parallel arrays (@keys:[...], @values:[...]).
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder forceMapOutputAsTwoArrays(boolean forceMapOutputAsTwoArrays) {
options.forceMapOutputAsTwoArrays = forceMapOutputAsTwoArrays;
return this;
}
/**
* @param allowNanAndInfinity boolean 'allowNanAndInfinity' setting. true will allow
* Double and Floats to be output as NAN and INFINITY, false
* and these values will come across as null.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder allowNanAndInfinity(boolean allowNanAndInfinity) {
options.allowNanAndInfinity = allowNanAndInfinity;
return this;
}
/**
* Option to write out enums as a String, it will write out the enum.name() field.
* This is the default way enums will be written out.
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder writeEnumsAsString() {
options.enumWriter = new Writers.EnumsAsStringWriter();
return this;
}
/**
* Option to write out all the member fields of an enum. You can also filter the
* field to write out only the public fields on the enum.
*
* @param writePublicFieldsOnly boolean, only write out the public fields when writing enums as objects
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder writeEnumAsJsonObject(boolean writePublicFieldsOnly) {
options.enumWriter = DefaultWriteOptions.nullWriter;
options.enumPublicFieldsOnly = writePublicFieldsOnly;
return this;
}
/**
* @param closeStream boolean set to 'true' to have JsonIo close the OutputStream when it is finished writing
* to it. The default is 'true'. If false, the OutputStream will not be closed, allowing
* you to continue writing further. Example, NDJSON that has new line eliminated JSON
* objects repeated.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder closeStream(boolean closeStream) {
options.closeStream = closeStream;
return this;
}
/**
* @param customWrittenClasses Map of Class to JsonWriter.JsonClassWriter. Establish the passed in Map as the
* established Map of custom writers to be used when writing JSON. Using this method
* more than once, will set the custom writers to only the values from the Set in
* the last call made.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder setCustomWrittenClasses(Map, JsonWriter.JsonClassWriter> customWrittenClasses) {
options.customWrittenClasses.clear();
addCustomWrittenClasses(customWrittenClasses);
return this;
}
/**
* @param customWrittenClasses Map of Class to JsonWriter.JsonClassWriter. Adds all custom writers into the custom
* writers map.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addCustomWrittenClasses(Map, JsonWriter.JsonClassWriter> customWrittenClasses) {
options.customWrittenClasses.putAll(customWrittenClasses);
return this;
}
/**
* @param clazz Class to add a custom writer for.
* @param customWriter JsonClassWriter to use when the passed in Class is encountered during serialization.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addCustomWrittenClass(Class> clazz, JsonWriter.JsonClassWriter customWriter) {
options.customWrittenClasses.put(clazz, customWriter);
return this;
}
/**
* Add a class to the not-customized list - the list of classes that you do not want to be picked up by a
* custom writer (that could happen through inheritance).
*
* @param notCustomClass Class to add to the not-customized list.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addNotCustomWrittenClass(Class> notCustomClass) {
options.notCustomWrittenClasses.add(notCustomClass);
return this;
}
/**
* @param notCustomClasses initialize the list of classes on the non-customized list. All prior associations
* will be dropped and this Collection will establish the new list.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder setNotCustomWrittenClasses(Collection> notCustomClasses) {
options.notCustomWrittenClasses.clear();
options.notCustomWrittenClasses.addAll(notCustomClasses);
return this;
}
/**
* @param clazz Class to add a single field to be included in the written JSON.
* @param includedFieldName String name of field to include in written JSON.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addIncludedField(Class> clazz, String includedFieldName) {
Convention.throwIfNull(includedFieldName, "includedFieldName cannot be null");
options.includedFieldNames.computeIfAbsent(clazz, k -> new LinkedHashSet<>()).add(includedFieldName);
return this;
}
/**
* @param clazz Class to add a Collection of fields to be included in written JSON.
* @param includedFieldNames Collection of String name of fields to include in written JSON.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addIncludedFields(Class> clazz, Collection includedFieldNames) {
options.includedFieldNames.computeIfAbsent(clazz, k -> new LinkedHashSet<>()).addAll(includedFieldNames);
return this;
}
/**
* @param includedFieldNames Map of Class's mapped to Collection of String field names to include in the written JSON.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addIncludedFields(Map, Collection> includedFieldNames) {
includedFieldNames.forEach(this::addIncludedFields);
return this;
}
/**
* @param clazz Class to add a single field to be excluded.
* @param excludedFieldName String name of field to exclude from written JSON.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addExcludedField(Class> clazz, String excludedFieldName) {
options.excludedFieldNames.computeIfAbsent(clazz, k -> new LinkedHashSet<>()).add(excludedFieldName);
return this;
}
/**
* @param clazz Class to add a Collection of fields to be excluded in written JSON.
* @param excludedFields Collection of String name of fields to exclude in written JSON.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addExcludedFields(Class> clazz, Collection excludedFields) {
options.excludedFieldNames.computeIfAbsent(clazz, k -> new LinkedHashSet<>()).addAll(excludedFields);
return this;
}
/**
* @param excludedFieldNames Map of Class's mapped to Collection of String field names to exclude from written JSON.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addExcludedFields(Map, Collection> excludedFieldNames) {
excludedFieldNames.forEach(this::addExcludedFields);
return this;
}
/**
* Change the date-time format to the ISO date format: "yyyy-MM-dd". This is for java.util.Data and
* java.sql.Date.
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder isoDateFormat() {
return dateTimeFormat(ISO_DATE_FORMAT);
}
/**
* Change the date-time format to the ISO date-time format: "yyyy-MM-dd'T'HH:mm:ss" (default). This is
* for java.util.Date and java.sql.Date.
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder isoDateTimeFormat() {
return dateTimeFormat(ISO_DATE_TIME_FORMAT);
}
/**
* Change the java.uti.Date and java.sql.Date format output to a "long," the number of seconds since Jan 1, 1970
* at midnight. Useful if you do not need to see the JSON, and want to keep the format smaller. This is for
* java.util.Date and java.sql.Date.
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder longDateFormat() {
addCustomWrittenClass(Date.class, new Writers.DateAsLongWriter());
return this;
}
/**
* Change the date-time format to the passed in format. The format pattens can be found in the Java Doc
* for the java.time.format.DateTimeFormatter class. There are many constants you can use, as well as
* the definition of how to construct your own patterns. This is for java.util.Date and java.sql.Date.
*
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder dateTimeFormat(String format) {
addCustomWrittenClass(Date.class, new Writers.DateWriter(format));
return this;
}
/**
* This option permits adding non-standard accessors (used when writing JSON) that access properties from objects,
* where the method name does not follow a standard setter/getter property name. For example, on java.time.Instance,
* to get the 'second' field, the accessor method is 'getEpochSecond()'.
* @param c Class that has the non-standard accessor. java.time.Instance in the example above.
* @param fieldName String name of the class property. 'second' in the example above.
* @param methodName The name of the non-standard method used to get the field value. 'getEpochSecond' in the example above.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addNonStandardGetter(Class> c, String fieldName, String methodName) {
Convention.throwIfNull(c, "class cannot be null");
Convention.throwIfNull(fieldName, "fieldName cannot be null");
Convention.throwIfNull(methodName, "methodName cannot be null");
options.nonStandardGetters.computeIfAbsent(c, cls -> new LinkedHashMap<>()).put(fieldName, methodName);
return this;
}
/**
* @param clazz class to add to be considered a non-referenceable object. Just like an "int" for example, any
* class added here will never use an @id/@ref pair. The downside, is that when read,
* each instance, even if the same as another, will use memory.
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addNonReferenceableClass(Class> clazz) {
Convention.throwIfNull(clazz, "clazz cannot be null");
options.nonRefClasses.add(clazz);
return this;
}
/**
* Add a custom option, which may be useful when writing custom writers. To remove a custom option, add the
* option with the appropriate key and null as the value.
* @param key String name of the custom option
* @param value Object value of the custom option
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addCustomOption(String key, Object value) {
if (key == null) {
throw new JsonIoException("Custom option key must not be null.");
}
if (value == null) {
options.customOptions.remove(key);
} else {
options.customOptions.put(key, value);
}
return this;
}
/**
* Add FieldFilter to the field filter chain. FieldFilters are presented a chance to eliminate a field by returning true
* from its boolean filter() method. If any FieldFilter returns true, the field is excluded.
* @param filterName String name of filter
* @param filter FieldFilter to add
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder addFieldFilter(String filterName, FieldFilter filter) {
options.fieldFilters.put(filterName, filter);
return this;
}
/**
* Remove named FieldFilter from the filter chain.
* @param filterName String name of FieldFilter to delete
* @return WriteOptionsBuilder for chained access.
*/
public WriteOptionsBuilder removeFieldFilter(String filterName) {
options.fieldFilters.remove(filterName);
return this;
}
/**
* Add MethodFilter to the filter chain. MethodFilters are presented a chance to eliminate a "getter" type accessing
* method by returning true from the boolean filter() method. If any MethodFilter returns true, the accessor method
* is excluded. This means it will drop back to use field access (attempting to access private field in same module
* with setAccessible()). This API is used when you want to write your own implementation of a MethodFilter.
* @param filterName String name of filter
* @param methodFilter {@link MethodFilter} to add
* @return {@link WriteOptionsBuilder} for chained access.
*/
public WriteOptionsBuilder addMethodFilter(String filterName, MethodFilter methodFilter) {
options.methodFilters.put(filterName, methodFilter);
return this;
}
/**
* Add a NamedMethodFilter to the filter chain. NamedMethodFilters are presented a chance to eliminate a "getter"
* type accessing method by returning true from the boolean filter() method. If the NamedMethodFilter matches a class
* and accessor method name, and it has no args, is public, and non-static, the accessor method is excluded. This
* means it will drop back to use field access (attempting to access private field in same module with setAccessible()).
* @param filterName String name of filter
* @param clazz Class containing method to filter
* @param methodName String name of method to not use for accessing value.
* @return {@link WriteOptionsBuilder} for chained access.
*/
public WriteOptionsBuilder addNamedMethodFilter(String filterName, Class> clazz, String methodName) {
options.methodFilters.put(filterName, new NamedMethodFilter(clazz, methodName));
return this;
}
/**
* Remove named MethodFilter from the method filter chain.
* @param filterName String name of filter
* @return {@link WriteOptionsBuilder} for chained access.
*/
public WriteOptionsBuilder removeMethodFilter(String filterName) {
options.methodFilters.remove(filterName);
return this;
}
/**
* Add AccessorFactory to the accessor factories chain. AccessFactories permit adding a standard pattern of
* locating "read" methods, for example, there is a built-in "get" and "is" AccessoryFactory.
* @param factoryName String name of accessor factory
* @param accessorFactory {@link AccessorFactory} to add
* @return {@link WriteOptionsBuilder} for chained access.
*/
public WriteOptionsBuilder addAccessorFactory(String factoryName, AccessorFactory accessorFactory) {
options.accessorFactories.put(factoryName, accessorFactory);
return this;
}
/**
* Remove named AccessorFactory from the access factories.
* @param factoryName String name of accessor filter
* @return {@link WriteOptionsBuilder} for chained access.
*/
public WriteOptionsBuilder removeAccessorFactory(String factoryName) {
options.accessorFactories.remove(factoryName);
return this;
}
/**
* Seal the instance of this class so that no more changes can be made to it.
*
* @return WriteOptionsBuilder for chained access.
*/
@SuppressWarnings("unchecked")
public WriteOptions build() {
options.clearCaches();
options.includedFieldNames = Collections.unmodifiableMap(options.includedFieldNames);
options.nonStandardGetters = Collections.unmodifiableMap(options.nonStandardGetters);
options.aliasTypeNames = Collections.unmodifiableMap(options.aliasTypeNames);
options.notCustomWrittenClasses = Collections.unmodifiableSet(options.notCustomWrittenClasses);
options.nonRefClasses = Collections.unmodifiableSet(options.nonRefClasses);
options.excludedFieldNames = Collections.unmodifiableMap(options.excludedFieldNames);
options.fieldFilters = Collections.unmodifiableMap(options.fieldFilters);
options.methodFilters = Collections.unmodifiableMap(options.methodFilters);
options.accessorFactories = Collections.unmodifiableMap(options.accessorFactories);
options.customWrittenClasses = Collections.unmodifiableMap(options.customWrittenClasses);
options.customOptions = Collections.unmodifiableMap(options.customOptions);
return options;
}
static class DefaultWriteOptions implements WriteOptions {
private boolean shortMetaKeys = false;
private ShowType showTypeInfo = WriteOptions.ShowType.MINIMAL;
private boolean prettyPrint = false;
private int lruSize = 1000;
private boolean writeLongsAsStrings = false;
private boolean skipNullFields = false;
private boolean forceMapOutputAsTwoArrays = false;
private boolean allowNanAndInfinity = false;
private boolean enumPublicFieldsOnly = false;
private boolean closeStream = true;
private JsonWriter.JsonClassWriter enumWriter = new Writers.EnumsAsStringWriter();
private ClassLoader classLoader = WriteOptions.class.getClassLoader();
private Map, Set> includedFieldNames = new LinkedHashMap<>();
private Map, Map> nonStandardGetters = new LinkedHashMap<>();
private Map aliasTypeNames = new LinkedHashMap<>();
private Set> notCustomWrittenClasses = new LinkedHashSet<>();
private Set> nonRefClasses = new LinkedHashSet<>();
private Map, Set> excludedFieldNames = new LinkedHashMap<>();
private Map fieldFilters = new LinkedHashMap<>();
private Map methodFilters = new LinkedHashMap<>();
private Map accessorFactories = new LinkedHashMap<>();
private Map, JsonWriter.JsonClassWriter> customWrittenClasses = new LinkedHashMap<>();
private Map customOptions = new LinkedHashMap<>();
// Runtime caches (not feature options), since looking up writers can be expensive
// when one does not exist, we cache the write or a nullWriter if one does not exist.
private final Map, JsonWriter.JsonClassWriter> writerCache = new ConcurrentHashMap<>(200, 0.8f, Runtime.getRuntime().availableProcessors());
// Creating the Accessors (methodHandles) is expensive so cache the list of Accessors per Class
private Map, List> accessorsCache = new LRUCache<>(lruSize);
private Map, Map> classMetaCache = new LRUCache<>(lruSize);
/**
* Default Constructor. Prevent instantiation outside of package.
*/
private DefaultWriteOptions() {
}
/**
* Alias Type Names, e.g. "ArrayList" instead of "java.util.ArrayList".
*
* @param typeName String name of type to fetch alias for. There are no default aliases.
* @return String alias name or null if type name is not aliased.
*/
public String getTypeNameAlias(String typeName) {
String alias = aliasTypeNames.get(typeName);
return alias == null ? typeName : alias;
}
/**
* @return Map of all aliases.
*/
public Map aliases() {
return Collections.unmodifiableMap(aliasTypeNames);
}
/**
* @return boolean true if set to always show type (@type)
*/
public boolean isAlwaysShowingType() {
return showTypeInfo == ShowType.ALWAYS;
}
/**
* @return boolean true if set to never show type (no @type)
*/
public boolean isNeverShowingType() {
return showTypeInfo == ShowType.NEVER;
}
/**
* @return boolean true if set to show minimal type (@type)
*/
public boolean isMinimalShowingType() {
return showTypeInfo == ShowType.MINIMAL;
}
/**
* @param clazz Class to check to see if there is a custom writer associated to it.
* @return boolean true if there is an associated custom writer class associated to the passed in class,
* false otherwise.
*/
public boolean isCustomWrittenClass(Class> clazz) {
return customWrittenClasses.containsKey(clazz);
}
/**
* @param clazz Class to see if it is on the not-customized list. Classes are added to this list when
* a class is being picked up through inheritance, and you don't want it to have a custom
* writer associated to it.
* @return boolean true if the passed in class is on the not-customized list, false otherwise.
*/
public boolean isNotCustomWrittenClass(Class> clazz) {
return notCustomWrittenClasses.contains(clazz);
}
public List getAccessorsForClass(final Class> c) {
return accessorsCache.computeIfAbsent(c, this::buildDeepAccessors);
}
/**
* @return boolean true if java.util.Date and java.sql.Date's are being written in long (numeric) format.
*/
public boolean isLongDateFormat() {
Object a = customWrittenClasses.get(Date.class);
return a instanceof Writers.DateAsLongWriter;
}
/**
* @param clazz Class to check to see if it is non-referenceable. Non-referenceable classes will always create
* a new instance when read in and never use @id/@ref. This uses more memory when the JSON is read in,
* as there will be a separate instance in memory for each occurrence. There are certain classes that
* json-io automatically treats as non-referenceable, like Strings, Enums, Class, and any Number
* instance (BigDecimal, AtomicLong, etc.) You can add to this list. Often, non-referenceable classes
* are useful for classes that can be defined in one line as a JSON, like a ZonedDateTime, for example.
* @return boolean true if the passed in class is considered a non-referenceable class.
*/
public boolean isNonReferenceableClass(Class> clazz) {
return nonRefClasses.contains(clazz) || // Covers primitives, primitive wrappers, Atomic*, Big*, String
Number.class.isAssignableFrom(clazz) ||
Date.class.isAssignableFrom(clazz) ||
String.class.isAssignableFrom(clazz) ||
clazz.isEnum();
}
/**
* @return boolean true if showing short meta-keys (@i instead of @id, @ instead of @ref, @t
* instead of @type, @k instead of @keys, @v instead of @values), false for full size. 'false' is the default.
*/
public boolean isShortMetaKeys() {
return shortMetaKeys;
}
/**
* @return boolean 'prettyPrint' setting, true being yes, pretty-print mode using lots of vertical
* white-space and indentations, 'false' will output JSON in one line. The default is false.
*/
public boolean isPrettyPrint() {
return prettyPrint;
}
/**
* @return int 'LRU Size' setting for holding reflected fields and accessors. Default is 1000.
*/
public int getLruSize() {
return lruSize;
}
/**
* @return boolean 'writeLongsAsStrings' setting, true indicating longs will be written as Strings,
* false to write them out as native JSON longs. Writing Strings as Longs to the JSON, will fix errors
* in Javascript when an 18-19 digit long value is sent to Javascript. This is because Javascript stores
* them in Doubles, which cannot handle the precision of an 18-19 digit long, but a String will retain
* the full value into Javascript. The default is false.
*/
public boolean isWriteLongsAsStrings() {
return writeLongsAsStrings;
}
/**
* @return boolean skipNullFields setting, true indicates fields with null values will not be written,
* false will still output the field with an associated null value. false is the default.
*/
public boolean isSkipNullFields() {
return skipNullFields;
}
/**
* @return boolean 'forceMapOutputAsTwoArrays' setting. true indicates that two arrays will be written to
* represent a Java Map, one for keys, one for values. false indicates one Java object will be used, if
* all the keys of the Map are Strings. If not, then the Map will be written out with a key array, and a
* parallel value array. (@keys:[...], @values:[...]). false is the default.
*/
public boolean isForceMapOutputAsTwoArrays() {
return forceMapOutputAsTwoArrays;
}
/**
* @return boolean will return true if NAN and Infinity are allowed to be written out for
* Doubles and Floats, else null will be written out. The default is false.
*/
public boolean isAllowNanAndInfinity() {
return allowNanAndInfinity;
}
/**
* true indicates that only public fields will be output on an Enum. Enums don't often have fields added to them
* but if so, then only the public fields will be written. The Enum will be written out in JSON object { } format.
* If there are not added fields to an Enum, it will be written out as a single line value. The default value
* is true. If you set this to false, it will change the 'enumFieldsAsObject' to true - because you will be
* showing potentially more than one value, it will require the enum to be written as an object.
*/
public boolean isEnumPublicFieldsOnly() {
return enumPublicFieldsOnly;
}
/**
* @return boolean 'true' if the OutputStream should be closed when the reading is finished. The default is 'true.'
*/
public boolean isCloseStream() {
return closeStream;
}
/**
* @return ClassLoader to be used when writing JSON to resolve String named classes.
*/
public ClassLoader getClassLoader() {
return classLoader;
}
/**
* Dummy place-holder class exists only because ConcurrentHashMap cannot contain a
* null value. Instead, singleton instance of this class is placed where null values
* are needed.
*/
private static final class NullClass implements JsonWriter.JsonClassWriter {
}
static final NullClass nullWriter = new NullClass();
/**
* Fetch the custom writer for the passed in Class. If it is cached (already associated to the
* passed in Class), return the same instance, otherwise, make a call to get the custom writer
* and store that result.
*
* @param c Class of object for which fetch a custom writer
* @return JsonClassWriter for the custom class (if one exists), null otherwise.
*/
public JsonWriter.JsonClassWriter getCustomWriter(Class> c) {
JsonWriter.JsonClassWriter writer = writerCache.computeIfAbsent(c, this::findCustomWriter);
return writer == nullWriter ? null : writer;
}
public JsonWriter.JsonClassWriter findCustomWriter(Class> c) {
JsonWriter.JsonClassWriter writer = MetaUtils.findClosest(c, customWrittenClasses, nullWriter);
return writer != nullWriter ? writer : MetaUtils.getClassIfEnum(c).isPresent() ? enumWriter : nullWriter;
}
public Object getCustomOption(String key)
{
return customOptions.get(key);
}
public Set getIncludedFields(Class> c) {
return Collections.unmodifiableSet(includedFieldNames.get(c));
}
public Set getExcludedFields(Class> c) {
return Collections.unmodifiableSet(excludedFieldNames.get(c));
}
public void clearCaches() {
classMetaCache.clear();
accessorsCache.clear();
}
private List buildDeepAccessors(final Class> clazz) {
final Map fields = getDeepDeclaredFields(clazz);
final List accessors = new ArrayList<>(fields.size());
for (final Map.Entry entry : fields.entrySet()) {
final Field field = entry.getValue();
final String uniqueFieldName = entry.getKey();
Accessor accessor = findMethodAccessor(field, uniqueFieldName);
if (accessor != null && isMethodFiltered(clazz, accessor.getFieldOrMethodName())) {
accessor = null;
}
if (accessor == null) {
accessor = Accessor.createFieldAccessor(field, uniqueFieldName);
}
if (accessor != null) {
accessors.add(accessor);
}
}
return Collections.unmodifiableList(accessors);
}
private boolean isMethodFiltered(Class> clazz, String methodName) {
for (MethodFilter filter : methodFilters.values()) {
if (filter.filter(clazz, methodName)) {
return true;
}
}
return false;
}
private Accessor findMethodAccessor(Field field, String uniqueFieldName) {
for (final AccessorFactory factory : accessorFactories.values()) {
try {
final Accessor accessor = factory.buildAccessor(field, nonStandardGetters, uniqueFieldName);
if (accessor != null) {
return accessor;
}
} catch (Throwable ignore) {
}
}
return null;
}
/**
* Gets the declared fields for the full class hierarchy of a given class
*
* @param c - given class.
* @return Map - map of string fieldName to Field Object. This will have the
* deep list of fields for a given class.
*/
public Map getDeepDeclaredFields(final Class> c) {
return classMetaCache.computeIfAbsent(c, this::buildWithIncludedMinusExcluded);
}
private Map buildWithIncludedMinusExcluded(Class> c) {
Convention.throwIfNull(c, "class cannot be null");
final Map map = new LinkedHashMap<>();
final Set excluded = new HashSet<>();
final Set includedFields = includedFieldNames.get(c);
final Set included = includedFields == null ? new HashSet<>() : includedFields;
Class> curr = c;
while (curr != null) {
List fields = ReflectionUtils.getDeclaredFields(curr);
final Set excludedForClass = this.excludedFieldNames.get(curr);
if (excludedForClass != null) {
excluded.addAll(excludedForClass);
}
for (Field field : fields) {
String fieldName = field.getName();
if (map.containsKey(fieldName)) {
fieldName = field.getDeclaringClass().getSimpleName() + '.' + fieldName;
}
// FieldFilter or 'Excluded listing' automatically eliminates field.
if (isFieldFiltered(field) || excluded.contains(fieldName)) {
continue;
}
boolean isTransient = Modifier.isTransient(field.getModifiers());
boolean includedExplicitly = included.contains(fieldName);
if (isTransient && !includedExplicitly) {
continue;
}
// If included not specified, then default to ALL, otherwise only consider explicitly included fields.
if (included.isEmpty() || includedExplicitly) {
map.put(fieldName, field);
}
}
curr = curr.getSuperclass();
}
return Collections.unmodifiableMap(map);
}
private boolean isFieldFiltered(Field field) {
for (FieldFilter filter : this.fieldFilters.values()) {
if (filter.filter(field)) {
return true;
}
}
return false;
}
}
/**
* Load custom writer classes based on contents of resources/customWriters.txt.
* Verify that classes listed are indeed valid classes loaded in the JVM.
*
* @return Map, JsonWriter.JsonClassWriter> containing the resolved Class -> JsonClassWriter instance.
*/
private static void loadBaseWriters() {
Map map = MetaUtils.loadMapDefinition("config/customWriters.txt");
ClassLoader classLoader = WriteOptions.class.getClassLoader();
for (Map.Entry entry : map.entrySet()) {
String className = entry.getKey();
String writerClassName = entry.getValue();
Class> clazz = ClassUtilities.forName(className, classLoader);
if (clazz == null) {
System.out.println("Class: " + className + " not defined in the JVM, so custom writer: " + writerClassName + ", will not be used.");
continue;
}
Class customWriter = (Class) ClassUtilities.forName(writerClassName, classLoader);
if (customWriter == null) {
System.out.println("Note: class not found (custom JsonClassWriter class): " + writerClassName + ", listed in resources/customWriters.txt as a custom writer for: " + className);
}
try {
JsonWriter.JsonClassWriter writer = customWriter.newInstance();
addPermanentWriter(clazz, writer);
} catch (Exception e) {
System.out.println("Note: class failed to instantiate (a custom JsonClassWriter class): " + writerClassName + ", listed in resources/customWriters.txt as a custom writer for: " + className);
}
}
}
/**
* Load the list of classes that are intended to be treated as non-referenceable, immutable classes.
*
* @return Set> which is the loaded from resource/nonRefs.txt and verified to exist in JVM.
*/
static void loadBaseNonRefs() {
Set set = MetaUtils.loadSetDefinition("config/nonRefs.txt");
set.forEach((className) -> {
Class> clazz = ClassUtilities.forName(className, WriteOptions.class.getClassLoader());
if (clazz == null) {
System.out.println("Class: " + className + " undefined. Cannot be used as non-referenceable class, listed in resources/nonRefs.txt");
}
addPermanentNonRef(clazz);
});
}
private static void loadBaseExcludedFields() {
Map, Set> allExcludedFields = ReadOptionsBuilder.loadClassToSetOfStrings("config/fieldsNotExported.txt");
for (Map.Entry, Set> entry : allExcludedFields.entrySet()) {
Class> clazz = entry.getKey();
Set excludedFields = entry.getValue();
for (String fieldName : excludedFields) {
addPermanentExcludedField(clazz, fieldName);
}
}
}
private static void loadBaseNonStandardGetters() {
Map, Map> nonStandardGetterMap = ReadOptionsBuilder.loadClassToFieldAliasNameMapping("config/nonStandardGetters.txt");
for (Map.Entry, Map> entry : nonStandardGetterMap.entrySet()) {
Class> clazz = entry.getKey();
Map pairingsMap = entry.getValue();
for (Map.Entry fieldToAltName : pairingsMap.entrySet()) {
addPermanentNonStandardGetter(clazz, fieldToAltName.getKey(), fieldToAltName.getValue());
}
}
}
}