Please wait. This can take some minutes ...
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.
com.blackbuild.klum.ast.util.layer3.StructureUtil Maven / Gradle / Ivy
/*
* The MIT License (MIT)
*
* Copyright (c) 2015-2024 Stephan Pauxberger
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.blackbuild.klum.ast.util.layer3;
import com.blackbuild.klum.ast.util.KlumInstanceProxy;
import groovy.lang.MetaProperty;
import groovy.lang.PropertyValue;
import groovy.lang.Tuple2;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import org.codehaus.groovy.tools.Utilities;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import static com.blackbuild.klum.ast.util.DslHelper.isDslObject;
/**
* Utility class for working with data structures. Provides methods to iterate through data structures and find
* specific ancestors or GPath expressions.
*/
public class StructureUtil {
private StructureUtil() {
//no instances
}
/**
* Returns the default name for a field of the given type. This is the uncapitalized simple name of the type. I.e.
* if a class Database had a single field of type UtilSchema, then the default name of such a field would be "utilSchema".
*
* @param type The class
* @return the uncapitalized named of the class.
*/
public static String toDefaultFieldName(Class> type) {
return StringGroovyMethods.uncapitalize(type.getSimpleName());
}
/**
* Returns the default name for the type of the given object.
*
* @param object The object to determine the default name
* @return The uncapitalized name of object's class
* @see #toDefaultFieldName(Class)
*/
public static String toDefaultFieldName(Object object) {
return toDefaultFieldName(object.getClass());
}
public static void visit(Object container, ModelVisitor visitor) {
visit(container, visitor, "");
}
public static void visit(Object container, ModelVisitor visitor, String path) {
doVisit(container, visitor, new ArrayList<>(), path, null);
}
private static void doVisit(Object element, ModelVisitor visitor, List alreadyVisited, String path, Object container) {
if (element == null) return;
if (element instanceof Collection)
doVisitCollection((Collection>) element, visitor, alreadyVisited, path, container);
else if (element instanceof Map)
doVisitMap((Map, ?>) element, visitor, alreadyVisited, path, container);
else
doVisitObject(element, visitor, alreadyVisited, path, container);
}
private static void doVisitObject(Object element, ModelVisitor visitor, List alreadyVisited, String path, Object container) {
if (!isDslObject(element)) return;
if (alreadyVisited.stream().anyMatch(v -> v == element)) return;
visitor.visit(path, element, container);
alreadyVisited.add(element);
ClusterModel.getFieldPropertiesStream(element)
.forEach(property -> doVisit(property.getValue(), visitor, alreadyVisited, path + "." + property.getName(), element));
}
private static void doVisitMap(Map, ?> map, ModelVisitor visitor, List alreadyVisited, String path, Object container) {
map.forEach((key, value) -> doVisit(value, visitor, alreadyVisited,path + "." + toGPath(key), container));
}
private static void doVisitCollection(Collection> collection, ModelVisitor visitor, List alreadyVisited, String path, Object container) {
AtomicInteger index = new AtomicInteger();
collection.forEach(member -> doVisit(member, visitor, alreadyVisited, path + "[" + index.getAndIncrement() + "]", container));
}
/**
* Iterates through a data structure and returns all fields of the given type.
*
* @param container The container from which to extract the types
* @param type The target type to retrieve
* @return a map of strings to objects
*/
public static Map deepFind(Object container, Class type) {
return deepFind(container, type, Collections.emptyList());
}
/**
* Iterates through a data structure and returns all fields of the given type.
*
* @param container The container from which to extract the types
* @param type The target type to retrieve
* @param ignoredTypes All types in this list are completely ignored, i.e. not visited
* @return a map of strings to objects
*/
public static Map deepFind(Object container, Class type, List> ignoredTypes) {
return deepFind(container, type, ignoredTypes, "");
}
/**
* Iterates through a data structure and returns all fields of the given type.
*
* @param container The container from which to extract the types
* @param type The target type to retrieve
* @param ignoredTypes All types in this list are completely ignored, i.e. not visited
* @param path The prefix to attach to the path
* @return a map of strings to objects
*/
public static Map deepFind(Object container, Class type, List> ignoredTypes, String path) {
return doDeepFind(container, type, ignoredTypes, path, new ArrayList<>());
}
protected static Map doDeepFind(Object container, Class type, List> ignoredTypes, String path, List visited) {
Map result = new HashMap<>();
if (container == null
|| ignoredTypes.stream().anyMatch(it -> it.isInstance(container))
|| visited.stream().anyMatch(it -> it == container))
return result;
visited.add(container);
if (type.isInstance(container)) {
//noinspection unchecked
result.put(path, (T) container);
return result;
}
if (container instanceof Collection) {
AtomicInteger index = new AtomicInteger();
((Collection>) container).forEach(member -> result.putAll(doDeepFind(member, type, ignoredTypes, path + "[" + index.getAndIncrement() + "]", visited)));
} else if (container instanceof Map) {
((Map, ?>) container).forEach((key, value) -> result.putAll(doDeepFind(value, type, ignoredTypes, path + "." + toGPath(key), visited)));
} else {
getNonIgnoredProperties(container).forEach((name, value) -> result.putAll(doDeepFind(value, type, ignoredTypes, path + "." + name, visited)));
}
return result;
}
static String toGPath(Object value) {
String text = value.toString();
return Utilities.isJavaIdentifier(text) ? text : InvokerHelper.inspect(text);
}
static Map getNonIgnoredProperties(Object container) {
Map result = new HashMap<>();
Class> type = container.getClass();
while (type != null) {
Arrays.stream(type.getDeclaredFields())
.filter(it -> !it.getName().contains("$"))
.forEach(it -> result.put(it.getName(), InvokerHelper.getProperty(container, it.getName())));
type = type.getSuperclass();
}
return result;
}
/**
* Returns the name of the field of the container containing the given object. If the object is not
* contained in a field, returns an empty Optional.
* @param container The container object to search
* @param child The child object to look for
* @return The name of the field containing the child object, or an empty Optional if the object is not contained in a field.
*/
public static Optional getPathOfFieldContaining(Object container, @NotNull Object child) {
Optional singleValuePath = getPathOfSingleField(container, child);
if (singleValuePath.isPresent()) return singleValuePath;
Optional collectionPath = getPathOfCollectionMember(container, child);
if (collectionPath.isPresent()) return collectionPath;
return getPathOfMapMember(container, child);
}
@NotNull
static Optional getPathOfMapMember(Object container, @NotNull Object child) {
//noinspection unchecked
return ClusterModel.getPropertiesStream(container, Map.class)
.filter(it -> ClusterModel.isMapOf(container, it, child.getClass()))
.map(it -> new Tuple2>(it.getName(), findKeyForValue((Map) it.getValue(), child)))
.filter(it -> it.getSecond().isPresent())
.map(it -> toGPath(it.getFirst()) + "." + toGPath(it.getSecond().get()))
.findFirst();
}
@NotNull
static Optional getPathOfCollectionMember(Object container, @NotNull Object child) {
return ClusterModel.getPropertiesStream(container, Collection.class)
.filter(it -> ClusterModel.isCollectionOf(container, it, child.getClass()))
.map(it -> new Tuple2<>(it.getName(), getIndexInCollection((Collection>) it.getValue(), child)))
.filter(it -> it.getSecond() != -1)
.map(it -> toGPath(it.getFirst()) + "[" + it.getSecond() + "]")
.findFirst();
}
@NotNull
public static Optional getPathOfSingleField(Object container, @NotNull Object child) {
return ClusterModel.getPropertiesStream(container, child.getClass())
.filter(it -> it.getValue() == child)
.map(PropertyValue::getName)
.map(StructureUtil::toGPath)
.findFirst();
}
static int getIndexInCollection(Collection> container, Object child) {
if (container instanceof List)
return ((List>) container).indexOf(child);
int index = 0;
for (Object element: container) {
if (element == child)
return index;
index++;
}
return -1;
}
static Optional findKeyForValue(Map map, @NotNull V value) {
return map.entrySet().stream()
.filter(it -> it.getValue() == value)
.map(Map.Entry::getKey)
.findFirst();
}
static boolean isNoInternalProperty(MetaProperty property) {
return !property.getName().contains("$");
}
/**
* Returns the full path of the given leaf object relative to its root element. This is determined by traversing
* the owner fields up until no more owner fields are encountered. Note that corner cases, like an object having two
* different owners or having only an owner method are not handled.
*
* @param leaf The object whose path is to be determined
* @return The full path of the object.
*/
public static @NotNull String getFullPath(@NotNull Object leaf) {
return getFullPath(leaf, null);
}
/**
* Returns the full path of the given leaf object relative to its root element. This is determined by traversing
* the owner fields up until no more owner fields are encountered. Note that corner cases, like an object having two
* different owners or having only an owner method are not handled. The optional rootPath will be prepended to
* the path. If leaf is not a DSL object or does not have an owner, an empty string is returned, regardless of the rootPath
* @param leaf The object whose path is to be determined
* @param rootPath on optional root element to prepend to the path
* @return The full path of the object.
*/
public static @NotNull String getFullPath(@NotNull Object leaf, @Nullable String rootPath) {
return createPath(leaf, null, rootPath);
}
static String createPath(Object child, Predicate stopCondition, String rootPath) {
final List ownerHierarchy = getOwnerHierarchy(child);
List truncatedList;
if (stopCondition != null) {
int indexOfAncestor = IntStream.range(0, ownerHierarchy.size())
.filter(i -> stopCondition.test(ownerHierarchy.get(i)))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Could not find matching ancestor"));
truncatedList = ownerHierarchy.subList(0, indexOfAncestor + 1);
} else {
truncatedList = ownerHierarchy;
}
Deque elements = hierarchyToPath(truncatedList);
if (rootPath != null)
elements.addFirst(rootPath);
return String.join(".", elements);
}
/**
* Returns the path from the container to the child object, but searching upwards through the owner objects
* of each layer until the container is found. If the root is reached without finding the container, or if the
* ancestor structure is invalid, an IllegalArgumentException is thrown.
* @param container The container object to search
* @param child The target of the path
* @return The path from the container to the child object
*/
public static String getRelativePath(Object container, Object child) {
return getRelativePath(container, child, null);
}
/**
* Returns the path from the container to the child object, but searching upwards through the owner objects
* of each layer until the container is found. If the root is reached without finding the container, or if the
* ancestor structure is invalid, an IllegalArgumentException is thrown.
* @param container The container object to search
* @param child The target of the path
* @param rootPath on optional root element to prepend to the path
* @return The path from the container to the child object
*/
public static String getRelativePath(Object container, Object child, String rootPath) {
return createPath(child, container::equals, rootPath);
}
/**
* Returns the path from the container to the child object, but searching upwards through the owner objects
* of each layer until an ancestor of the given type is found. If the root is reached without finding the container, or if the
* ancestor structure is invalid, an IllegalArgumentException is thrown.
* @param containerType The type of ancestor to be searched for
* @param child The target of the path
* @return The path from the container to the child object
*/
public static String getRelativePath(Class> containerType, Object child) {
return getRelativePath(containerType, child, null);
}
/**
* Returns the path from the container to the child object, but searching upwards through the owner objects
* of each layer until an ancestor of the given type is found. If the root is reached without finding the container, or if the
* ancestor structure is invalid, an IllegalArgumentException is thrown.
* @param containerType The type of ancestor to be searched for
* @param child The target of the path
* @param rootPath on optional root element to prepend to the path
* @return The path from the container to the child object
*/
public static String getRelativePath(Class> containerType, Object child, String rootPath) {
return createPath(child, containerType::isInstance, rootPath);
}
/**
* Returns the ancestor of the given type for the given child object. If the child object is not a DSL object or has
* no ancestor of the given type, an empty optional is returned.
* @param child The child whose ancestor is to be found
* @param type The type of ancestor to be found
* @return The ancestor of the given type, or an empty optional if none was found
* @param The type of ancestor to be found
*/
public static Optional getAncestorOfType(Object child, Class type) {
//noinspection unchecked
return (Optional) getOwnerHierarchy(child).stream()
.filter(type::isInstance)
.findFirst();
}
public static Deque hierarchyToPath(List hierarchy) {
Deque result = new ArrayDeque<>();
for (int i = hierarchy.size() - 1; i > 0; i--) {
Object owner = hierarchy.get(i);
Object child = hierarchy.get(i - 1);
String path = getPathOfFieldContaining(owner, child).orElseThrow(() -> new IllegalStateException("Object " + owner + " does not contain " + child));
result.add(path);
}
return result;
}
/**
* Returns the owner hierarchy of the given leaf object, starting with the leaf object itself and ending with the root object.
* Throws an IllegalStateException if an object in the hierarchy contains more than one owner or if the hierarchy contains a cycle.
* @param leaf The leaf object
* @return The owner hierarchy of the leaf object
*/
public static List getOwnerHierarchy(Object leaf) {
List result = new ArrayList<>();
while (isDslObject(leaf)) {
if (result.contains(leaf))
throw new IllegalStateException("Object " + leaf + " has an owner cycle");
result.add(leaf);
leaf = KlumInstanceProxy.getProxyFor(leaf).getSingleOwner();
}
return result;
}
}