/**
*
* Copyright (c) 2006-2017, Speedment, Inc. 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.
*/
package com.speedment.runtime.config.util;
import com.speedment.common.mapstream.MapStream;
import com.speedment.runtime.config.Document;
import com.speedment.runtime.config.internal.util.Trees;
import com.speedment.runtime.config.trait.HasAlias;
import com.speedment.runtime.config.trait.HasName;
import com.speedment.runtime.config.trait.HasParent;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import static java.util.Objects.requireNonNull;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import java.util.stream.Stream;
/**
* Common utility methods for working with instances of the {@code Document}
* interface.
*
* @author Per Minborg
* @author Emil Forslund
* @since 2.3.0
*/
public final class DocumentUtil {
/**
* Traverses all the documents at and below the specified document in a
* tree. Traversal is done depth first. The order of sub-document traversal
* within a specific Document is unspecified (For example, a tables columns
* may be traversed in any order).
*
* @param document the document to start at
* @return stream of descendants
*/
@SuppressWarnings("unchecked")
public static Stream<? extends Document> traverseOver(Document document) {
requireNonNull(document);
return Trees.traverse(
document,
d -> d.children(),
Trees.TraversalOrder.DEPTH_FIRST_PRE
);
}
/**
* Returns the first ancestor found of the specified type to the specified
* document when walking up the tree. If there was no ancestor of the
* specified type and the root was reached, an empty {@code Optional} is
* returned.
*
* @param <E> ancestor type
* @param document the starting point
* @param clazz the ancestor type to look for
* @return first ancestor found or empty
*/
public static <E extends Document> Optional<E> ancestor(
final Document document,
final Class<E> clazz) {
requireNonNull(document);
requireNonNull(clazz);
return document.ancestors()
.filter(clazz::isInstance)
.map(clazz::cast)
.findFirst();
}
/**
* Returns a stream of child documents to a specified document by using the
* supplied constructor.
*
* @param <E> the expected child type
* @param document the parent document
* @param childConstructor child constructor
* @return stream of children
*/
@SuppressWarnings("unchecked")
public static <E extends Document> Stream<E> childrenOf(
final Document document,
final BiFunction<Document, Map<String, Object>, E> childConstructor) {
requireNonNull(document);
requireNonNull(childConstructor);
return document.getData().values().stream()
.filter(obj -> obj instanceof List<?>)
.map(list -> (List<Object>) list)
.flatMap(list -> list.stream())
.filter(obj -> obj instanceof Map<?, ?>)
.map(map -> (Map<String, Object>) map)
.map(map -> childConstructor.apply(document, map));
}
/**
* Creates and returns a new raw map on a specified key in the specified
* document. This might involve creating a new list if no such existed
* already. If children already existed on that key, the new one is simply
* added to the end of the list.
*
* @param parent the parent to create it in
* @param key the key to create it under
* @return the newly creating raw child map
*/
public static Map<String, Object> newDocument(Document parent, String key) {
requireNonNull(parent);
requireNonNull(key);
final List<Map<String, Object>> children = parent.get(key)
.map(DocumentUtil::castToDocumentList)
.orElseGet(() -> {
final List<Map<String, Object>> list =
new CopyOnWriteArrayList<>();
parent.put(key, list);
return list;
});
final Map<String, Object> child = new ConcurrentSkipListMap<>();
children.add(child);
return child;
}
/**
* An enumeration of the types of names that documents can have. This is
* used to control which method should be called when parsing the document
* into a name.
*/
public enum Name {
/**
* The name used in the database to reference this document.
*/
DATABASE_NAME,
/**
* A user defined name that is used for the document primarily in
* generated code.
*/
JAVA_NAME;
/**
* Returns the appropriate name of the specified document.
*
* @param document the document
* @return the name
*/
public String of(HasAlias document) {
switch (this) {
case DATABASE_NAME:
return document.getName();
case JAVA_NAME:
return document.getJavaName();
default:
throw new UnsupportedOperationException(
"Unknown enum constant '" + name() + "'."
);
}
}
}
/**
* Returns the relative name for the given Document up to the point given by
* the parent Class.
* <p>
* For example, {@code relativeName(column, Dbms.class, DATABASE_NAME)}
* would return the String "dbms_name.schema_name.table_name.column_name".
*
* @param <T> parent type
* @param <D> document type
* @param document to use
* @param from the document type to get the name from
* @param name if java or database name should be used
* @return the relative name for this Node from the point given by the
* parent Class
*/
public static <T extends Document & HasName, D extends Document & HasName>
String relativeName(D document, Class<T> from, Name name) {
return relativeName(document, from, name, Function.identity());
}
/**
* Returns the relative name for the given Document up to the point given by
* the parent Class by successively applying the provided nameMapper onto
* the Node names.
* <p>
* For example, {@code relativeName(column, Dbms.class, DATABASE_NAME)}
* would return the String "dbms_name.schema_name.table_name.column_name".
*
* @param <T> parent type
* @param <D> Document type
* @param document to use
* @param from class
* @param name if java or database name should be used
* @param nameMapper to apply to all names encountered during traversal
* @return the relative name for this Node from the point given by the
* parent Class
*/
public static <T extends Document & HasName, D extends Document & HasName>
String relativeName(
D document,
Class<T> from,
Name name,
Function<String, String> nameMapper) {
return relativeName(document, from, name, ".", nameMapper);
}
/**
* Returns the relative name for the given Document up to the point given by
* the parent Class by successively applying the provided nameMapper onto
* the Node names and separating the names with the provided separator.
* <p>
* For example, {@code relativeName(column, Dbms.class)} would return the
* String "dbms_name.schema_name.table_name.column_name" if the separator is
* "."
*
* @param <T> parent type
* @param <D> Document type
* @param document to use
* @param from class
* @param name if java or database name should be used
* @param separator to use between the document names
* @param nameMapper to apply to all names encountered during traversal
* @return the relative name for this Node from the point
* given by the parent Class
*/
public static <T extends Document & HasName, D extends Document & HasName>
String relativeName(
final D document,
final Class<T> from,
final Name name,
final CharSequence separator,
final Function<String, String> nameMapper) {
requireNonNull(document);
requireNonNull(from);
requireNonNull(nameMapper);
final StringJoiner sj = new StringJoiner(separator).setEmptyValue("");
final List<HasAlias> ancestors = document.ancestors()
.map(HasAlias::of)
.collect(toList());
boolean add = false;
for (final HasAlias parent : ancestors) {
if (add || parent.mainInterface().isAssignableFrom(from)) {
sj.add(nameMapper.apply(name.of(parent)));
add = true;
}
}
sj.add(nameMapper.apply(name.of(HasAlias.of(document))));
return sj.toString();
}
/**
* Creates a deep copy of the raw map in the specified document and wrap it
* in a new typed document using the specified constructor.
*
* @param <DOC> the document type
* @param document the document
* @param constructor the document constructor
* @return the copy
*/
public static <DOC extends Document> DOC deepCopy(
DOC document,
Function<Map<String, Object>, DOC> constructor) {
return constructor.apply(deepCopyMap(document.getData()));
}
/**
* Creates a deep copy of the raw map in the specified document and wrap it
* in a new typed document using the specified constructor.
*
* @param <P> the parent type
* @param <DOC> the document type
* @param document the document
* @param constructor the document constructor
* @return the copy
*/
public static <P extends Document, DOC extends Document & HasParent<P>>
DOC deepCopy(DOC document, BiFunction<P, Map<String, Object>, DOC> constructor) {
return constructor.apply(
document.getParent().orElse(null),
deepCopyMap(document.getData())
);
}
/**
* Returns an {@code Exception} supplier for when no attribute could be
* found on a specified key in a specified document.
*
* @param document the document
* @param key the key
* @return the {@code Exception} supplier
*/
public static Supplier<NoSuchElementException> newNoSuchElementExceptionFor(
Document document,
String key) {
return () -> new NoSuchElementException(
"An attribute with the key '" + key
+ "' could not be found in " + document
+ " with name (" + Optional.ofNullable(document)
.flatMap(doc -> doc.getAsString("name"))
.orElse("null")
+ ")"
);
}
/**
* Helps documents to format a {@code toString()}-method.
*
* @param document the document
* @return the string
*/
public static String toStringHelper(Document document) {
return document.getClass().getSimpleName()
+ " {"
+ MapStream.of(document.getData())
.mapValue(VALUE_MAPPER)
.map((k, v) -> "\"" + k + "\": " + v.toString())
.collect(joining(", "))
+ "}";
}
/**
* Casts the specified object to a {@code List<Map<String, Object>>}.
* <p>
* Careful! This method does not cause any unchecked warnings, so it might
* be unsafe to use.
*
* @param obj the object to cast
* @return the typed list
*/
public static List<Map<String, Object>> castToDocumentList(Object obj) {
@SuppressWarnings("unchecked")
final List<Map<String, Object>> list = (List<Map<String, Object>>) obj;
return list;
}
private static <K, V> Map<K, V> deepCopyMap(Map<K, V> original) {
final Map<K, V> copy = new ConcurrentSkipListMap<>();
MapStream.of(original)
.mapValue(DocumentUtil::deepCopyObject)
.forEachOrdered(copy::put);
return copy;
}
private static <V> List<V> deepCopyList(List<V> original) {
final List<V> copy = new CopyOnWriteArrayList<>();
original.stream()
.map(DocumentUtil::deepCopyObject)
.forEachOrdered(copy::add);
return copy;
}
private static <V> V deepCopyObject(V original) {
if (String.class.isAssignableFrom(original.getClass())
|| Number.class.isAssignableFrom(original.getClass())
|| Boolean.class.isAssignableFrom(original.getClass())
|| Enum.class.isAssignableFrom(original.getClass())) {
return original;
} else if (List.class.isAssignableFrom(original.getClass())) {
@SuppressWarnings("unchecked")
final V result = (V) deepCopyList((List<?>) original);
return result;
} else if (Map.class.isAssignableFrom(original.getClass())) {
@SuppressWarnings("unchecked")
final V result = (V) deepCopyMap((Map<?, ?>) original);
return result;
} else {
throw new UnsupportedOperationException(
"Can't deep copy unknown type '" + original.getClass() + "'."
);
}
}
private static final Function<Object, Object> VALUE_MAPPER = o -> {
if (o instanceof List) {
return "[" + ((List) o).size() + "]";
} else {
return o;
}
};
/**
* Utility classes should not be instantiated.
*/
private DocumentUtil() {
throw new UnsupportedOperationException();
}
}