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.exadel.aem.toolkit.plugin.handlers.placement.PlacementCollisionSolver Maven / Gradle / Ivy
Go to download
Maven plugin for storing AEM (Granite UI) markup created with Exadel Toolbox Authoring Kit
/*
* 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.exadel.aem.toolkit.plugin.handlers.placement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import com.exadel.aem.toolkit.api.handlers.MemberSource;
import com.exadel.aem.toolkit.api.handlers.Source;
import com.exadel.aem.toolkit.api.handlers.Target;
import com.exadel.aem.toolkit.core.CoreConstants;
import com.exadel.aem.toolkit.plugin.adapters.ResourceTypeSetting;
import com.exadel.aem.toolkit.plugin.exceptions.InvalidLayoutException;
import com.exadel.aem.toolkit.plugin.handlers.placement.registries.SectionsRegistry;
import com.exadel.aem.toolkit.plugin.maven.PluginRuntime;
import com.exadel.aem.toolkit.plugin.sources.ModifiableMemberSource;
import com.exadel.aem.toolkit.plugin.utils.NamingUtil;
import com.exadel.aem.toolkit.plugin.utils.ordering.OrderingUtil;
/**
* Contains helper methods for the {@link PlacementHelper} that resolve collisions between Java class members that
* create Granite UI widgets or containers. These methods are aimed at avoiding ambiguities in naming and/or reporting
* to the user of potential rendering problems
*/
class PlacementCollisionSolver {
private static final String TYPE_FIELD = "Field";
private static final String TYPE_METHOD = "Method";
private static final String NAMING_COLLISION_MESSAGE_TEMPLATE = "%s named \"%s\" in class \"%s\" "
+ "collides with the %s named \"%s\" in class \"%s\" (%s). This may cause unexpected behavior";
private static final String REASON_AMBIGUOUS_ORDER = "attributes of the parent class member will have precedence";
private static final String REASON_DIFFERENT_RESTYPE = "different resource types provided";
private static final String CIRCULAR_PLACEMENT_MESSAGE_TEMPLATE = "%s named \"%s\" in class \"%s\" "
+ "requests to be placed in container declared by %s named \"%s\" in class \"%s\" while the latter is already "
+ "a child container of the first";
/**
* Default (instantiation-preventing) constructor
*/
private PlacementCollisionSolver() {
}
/* ---------------------
Collisions management
---------------------*/
/**
* Tests the provided collection of members for possible collisions (Java class members that produce the same tag
* name), and throws an exception if: - a member from a superclass is positioned after the same-named member
* from a subclass, therefore, will "shadow" it and produce unexpected UI display; - a member from a class has
* a resource type other than of a same-named member from a superclass or interface, therefore, is at risk of
* producing a "mixed" markup
* @param sources {@code List} of sources available for rendering
*/
public static void checkForCollisions(List sources) {
List distinctNames = sources
.stream()
.map(NamingUtil::stripGetterPrefix)
.distinct()
.collect(Collectors.toList());
if (distinctNames.size() == sources.size()) {
// All names are different: there are no collisions
return;
}
for (String name : distinctNames) {
checkForNameCollisions(sources, name);
checkForResourceTypeCollisions(sources, name);
}
}
/**
* Tests the provided collection of member sources sharing the particular name for naming collisions. If a collision
* is found, the {@link InvalidLayoutException} is thrown
* @param sources {@code List} of sources available for rendering
* @param name String representing a common name of sources being tested
*/
private static void checkForNameCollisions(List sources, String name) {
LinkedList sameNameMembers = getMembersWithSameName(sources, name);
LinkedList sameNameMembersByOrigin = sameNameMembers
.stream()
.sorted(OrderingUtil::compareByOrigin)
.collect(Collectors.toCollection(LinkedList::new));
if (!sameNameMembers.getLast().equals(sameNameMembersByOrigin.getLast())) {
reportCollision(
sameNameMembersByOrigin.getLast().adaptTo(MemberSource.class),
sameNameMembers.getLast().adaptTo(MemberSource.class),
REASON_AMBIGUOUS_ORDER);
}
}
/**
* Tests the provided collection of member sources sharing the particular name for collisions in exposed resource
* types. If a collision is found, the {@link InvalidLayoutException} is thrown
* @param sources {@code List} of sources available for rendering
* @param name String representing the common name of the sources being tested
*/
private static void checkForResourceTypeCollisions(List sources, String name) {
List sameNameMembers = getMembersWithSameName(sources, name);
// We consider fields separately from methods
// because we allow that a field, and a method of the same name have different resource types
Predicate fieldPredicate = PlacementCollisionSolver::isField;
Predicate methodPredicate = PlacementCollisionSolver::isMethod;
for (Predicate currentMemberType : Arrays.asList(fieldPredicate, methodPredicate)) {
Map membersByResourceType = sameNameMembers
.stream()
.filter(currentMemberType)
.collect(Collectors.toMap(
source -> source.adaptTo(ResourceTypeSetting.class).getValue(),
source -> source,
(first, second) -> second,
LinkedHashMap::new));
if (membersByResourceType.size() > 1) {
Source[] competitorArray = membersByResourceType.values().toArray(new Source[0]);
reportCollision(
competitorArray[1].adaptTo(MemberSource.class),
competitorArray[0].adaptTo(MemberSource.class),
REASON_DIFFERENT_RESTYPE);
}
}
}
/**
* Throws a formatted exception whenever a collision is found
* @param first {@code MemberSource} instance representing the first member of a collision
* @param second {@code MemberSource} instance representing the second member of a collision
* @param reason String explaining the essence of the collision
*/
private static void reportCollision(MemberSource first, MemberSource second, String reason) {
PluginRuntime
.context()
.getExceptionHandler()
.handle(new InvalidLayoutException(String.format(
NAMING_COLLISION_MESSAGE_TEMPLATE,
isField(first) ? TYPE_FIELD : TYPE_METHOD,
first.getName(),
first.getDeclaringClass().getSimpleName(),
isField(second) ? TYPE_FIELD.toLowerCase() : TYPE_METHOD.toLowerCase(),
second.getName(),
second.getDeclaringClass().getSimpleName(),
reason)));
}
/* ------------------------------
Naming coincidences management
------------------------------ */
/**
* Checks for cases when sources intended to be placed in the same container differ in {@code type} (e.g., one is a
* Java method, another is field) but share the same name. If same-named sources have different resource types, the
* tag names of field-bound sources are left the same while the methods' names are changed. This is the special
* case which allows the user to place a "service" annotation such as {@code @Heading} on the class or interface
* method while placing a "traditional" annotation such as {@code @TextField} on the field
* @param sources List of sources, such as members of a Java class
*/
public static void resolveFieldMethodNameCoincidences(List sources) {
List fields = sources
.stream()
.filter(PlacementCollisionSolver::isField)
.collect(Collectors.toList());
for (Source currentField : fields) {
List methodsToRename = getMembersWithSameName(
sources,
currentField.getName(),
member -> isMethod(member)
&& isSameOrSuperClass(currentField, member)
&& hasDifferentResourceType(currentField, member)
);
if (methodsToRename.isEmpty()) {
continue;
}
Map> methodGroupsByResourceType = new HashMap<>();
methodsToRename.forEach(method -> methodGroupsByResourceType.computeIfAbsent(
method.adaptTo(ResourceTypeSetting.class).getValue(),
key -> new ArrayList<>())
.add(method));
for (Map.Entry> methodGroupByResourceType : methodGroupsByResourceType.entrySet()) {
List methodGroup = methodGroupByResourceType.getValue();
String simpleResourceType = StringUtils.substringAfterLast(methodGroupByResourceType.getKey(), CoreConstants.SEPARATOR_SLASH);
String newName = currentField.getName() + CoreConstants.SEPARATOR_UNDERSCORE + simpleResourceType.toLowerCase();
methodGroup.forEach(method -> method.adaptTo(ModifiableMemberSource.class).setName(newName));
}
}
}
/**
* Detects whether the two provided {@code Source}s represent classes that are the same or else are
* in the "child - parent" relation
* @param first {@code Source} instance representing a class member
* @param second {@code Source} instance representing a class member
* @return True or false
*/
private static boolean isSameOrSuperClass(Source first, Source second) {
Class> firstClass = first.adaptTo(MemberSource.class).getDeclaringClass();
Class> secondClass = second.adaptTo(MemberSource.class).getDeclaringClass();
return ClassUtils.isAssignable(firstClass, secondClass);
}
/**
* Detects whether the two provided {@code Source}s represent Granite UI components with the different resource types
* @param first {@code Source} instance representing a class member
* @param second {@code Source} instance representing a class member
* @return True or false
*/
private static boolean hasDifferentResourceType(Source first, Source second) {
return !first.adaptTo(ResourceTypeSetting.class).getValue().equals(second.adaptTo(ResourceTypeSetting.class).getValue());
}
/* -----------------------------
Circular placement management
----------------------------- */
/**
* Detects circular reference ambiguities in multi-section Granite UI buildups such as when member A requests (via,
* e.g., its {@code Place} directive) to be placed within a section declared by member B while member B is to be
* placed within a container created upon member A
* @param host {@code Source} object that represents the current host (the element for which child
* nodes are being collected)
* @param hostContainer {@code Target} object referring to the node that matches the {@code host}
* @param candidate {@code Source} object representing a potential child {@code host}
* @param candidateContainer {@code Target} object referring to the node that matches the {@code candidate}
*/
public static void checkForCircularPlacement(
Source host,
Target hostContainer,
Source candidate,
Target candidateContainer) {
if (isProneToCircularPlacement(host, hostContainer, candidate, candidateContainer)
&& isProneToCircularPlacement(candidate, candidateContainer, host, hostContainer)) {
PluginRuntime
.context()
.getExceptionHandler()
.handle(new InvalidLayoutException(String.format(
CIRCULAR_PLACEMENT_MESSAGE_TEMPLATE,
isField(candidate) ? TYPE_FIELD : TYPE_METHOD,
candidate.getName(),
((MemberSource) candidate).getDeclaringClass().getSimpleName(),
isField(host) ? TYPE_FIELD.toLowerCase() : TYPE_METHOD.toLowerCase(),
host.getName(),
((MemberSource) host).getDeclaringClass().getSimpleName())));
}
}
/**
* Called by {@link PlacementCollisionSolver#checkForCircularPlacement(Source, Target, Source, Target)} to perform
* an elementary check of the second member being able to be nested in the container built upon the first member. A
* full circular reference detection needs to run these checks in pair with arguments swapped
* @param first {@code Source} object that represents the first member of the comparison
* @param firstContainer {@code Target} object referring to the node that matches the {@code first}
* @param second {@code Source} object that represents the second member of the comparison
* @param secondContainer {@code Target} object referring to the node that matches the {@code candidate}
* @return True if {@code second} can be placed into {@code first}; otherwise, false
*/
private static boolean isProneToCircularPlacement(
Source first,
Target firstContainer,
Source second,
Target secondContainer) {
if (!SectionsRegistry.isAvailableFor(first) || !SectionsRegistry.isAvailableFor(second)) {
return false;
}
if (SectionsRegistry.from(second, secondContainer).getAvailable().stream().anyMatch(section -> section.canContain(first))) {
return true;
}
return secondContainer.findChild(child -> child.equals(firstContainer)) != null;
}
/* ----------------------
Common utility methods
----------------------*/
/**
* Retrieves the list of {@code Source} objects matching the provided name. Names of fields and methods are coerced,
* e.g., both {@code private String text;} and {@code public String getText() {...}} are considered sharing the same
* name
* @param sources {@code List} of sources available for rendering
* @param name String representing the common name of sources to select
* @return An ordered list of {@code Source} objects
*/
private static LinkedList getMembersWithSameName(List sources, String name) {
return getMembersWithSameName(sources, name, null);
}
/**
* Retrieves the list of {@code Source} objects matching the provided name. Names of fields and methods are coerced,
* e.g., both {@code private String text;} and {@code public String getText() {...}} are considered sharing the same
* name
* @param sources {@code List} of sources available for rendering
* @param name String representing the common name of sources to select
* @param filter Nullable {@code Predicate} used as the additional filter of matching sources
* @return An ordered list of {@code Source} objects
*/
private static LinkedList getMembersWithSameName(List sources, String name, Predicate filter) {
return sources
.stream()
.filter(source -> StringUtils.equals(NamingUtil.stripGetterPrefix(source), name))
.filter(source -> filter == null || filter.test(source))
.map(source -> source.adaptTo(MemberSource.class))
.collect(Collectors.toCollection(LinkedList::new));
}
/**
* Used by the naming coincidences resolution logic to detect whether the current {@code Source} instance represents
* a Java class field
* @param source {@code Source} object
* @return True or false
*/
private static boolean isField(Source source) {
return source.adaptTo(Field.class) != null;
}
/**
* Used by the naming coincidences resolution logic to detect whether the current {@code Source} instance represents
* a Java class method
* @param source {@code Source} object
* @return True or false
*/
private static boolean isMethod(Source source) {
return source.adaptTo(Method.class) != null;
}
}