/** * Copyright 2013-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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.roboconf.dm.templating.internal.helpers; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.logging.Logger; import net.roboconf.core.utils.Utils; import net.roboconf.dm.templating.internal.contexts.InstanceContextBean; /** * An instance filter based on a component path and/or an installer name. * <p> * This filter is used in the {@linkplain AllHelper all} Handlebars template helper. * </p> * * @author Pierre Bourret - Université Joseph Fourier */ public final class InstanceFilter { public static final String JOKER = "*"; public static final String PATH_SEPARATOR = "/"; public static final String ALTERNATIVE = "|"; private final String path; private final Node rootNode; private final String installerName; /** * Private constructor. * @param path component path * @param rootNode the root node * @param installerName the installer name (can be null) */ private InstanceFilter( String path, Node rootNode, String installerName ) { this.path = path; this.rootNode = rootNode; this.installerName = installerName; } /** * Factory for {@code Filter}s. * * @param path component path for the filter to be created (neither null, nor empty) * @param installerName * @return the created filter * @throws IllegalArgumentException if {@code path} is an illegal component path */ public static InstanceFilter createFilter( final String path, String installerName ) { List<String> types = Utils.splitNicely( path, ALTERNATIVE ); OrNode rootNode = new OrNode(); for( String type : types ) { Node node = buildNodeForPath( type ); rootNode.delegates.add( node ); } return new InstanceFilter( path, rootNode, installerName ); } /** * Applies this filter to the given instances. * * @param instances the collection of instances to which this filter has to be applied. * @return the instances of the provided collection that matches this filter. */ public Collection<InstanceContextBean> apply( Collection<InstanceContextBean> instances ) { final ArrayList<InstanceContextBean> result = new ArrayList<InstanceContextBean> (); for( InstanceContextBean instance : instances ) { boolean installerMatches = this.installerName == null || this.installerName.equalsIgnoreCase( instance.getInstaller()); if( installerMatches && this.rootNode.isMatching( instance )) result.add( instance ); } result.trimToSize(); return Collections.unmodifiableCollection(result); } /** * Gets the path string of this filter. * @return the path string of this filter. */ public String getPath() { return this.path; } /** * Builds a node for a single type (meaning no '|' in the path). * @param path a non-null type * @return a non-null node */ private static Node buildNodeForPath( String path ) { // The path cannot be empty, guaranteed by AllHelper final String[] elements = path.split(PATH_SEPARATOR, -1); final int last = elements.length - 1; // Iterate in reverse order, as the instances we want are those on the right side of the path. AndNode rootNode = null; AndNode parentNode = null; for (int i = last; i >= 0; i--) { final String element = elements[i]; // Sanity checks if( element.isEmpty()) { Logger logger = Logger.getLogger( InstanceFilter.class.getName()); logger.warning( "An invalid component path was found in templates. Wrong part is: '" + path + "'." ); rootNode = null; break; } // Node for the current element. final AndNode currentNode = new AndNode(); // Type name filter if (!JOKER.equals(element)) currentNode.delegates.add(new TypeNode(element)); // Special handling if the path begins with a leading '/'. // It means that the instance must be a root instance. // In this case the first element is empty "", despite the component path remains valid. // The following element (at index 1) must match root instances only. if (i == 1 && elements[0].isEmpty()) { currentNode.delegates.add(new RootInstanceNode()); // The next element is an empty string. // If we don't shortcut it, it will make the next iteration fail. // Skip it by forcing the index to its ending bound. i = 0; } if (rootNode == null) { // The current node is the root node. We have no parent node yet. rootNode = currentNode; } else { // Chain the current node with its parent node. parentNode.delegates.add(new ParentInstanceNode(currentNode)); } // Shift and reiterate. parentNode = currentNode; } return rootNode != null ? rootNode : new ErrorNode(); } /** * Basic element of an instance filter. */ private static abstract class Node { /** * Tests if an instance matches this instance filter node. * * @param instance the instance to test. * @return {@code true} if and only if the given instance matches this instance filter node. */ abstract boolean isMatching( InstanceContextBean instance ); } /** * A node indicating an error was encountered while processing this path. */ private static class ErrorNode extends Node { @Override boolean isMatching( InstanceContextBean instance ) { return false; } } /** * And-combination of several nodes. * <p> * As a corner-case property, an {@code AndNode} without delegates matches any instance. It is used to handle the * special {@value #JOKER} in a path element. * </p> */ private static class AndNode extends Node { final LinkedList<Node> delegates = new LinkedList<Node>(); @Override boolean isMatching( final InstanceContextBean instance ) { boolean matching = true; for( Iterator<Node> it = this.delegates.iterator(); it.hasNext() && matching; ) matching = it.next().isMatching( instance ); return matching; } } /** * Or-combination of several nodes. * <p> * As a corner-case property, an {@code OrNode} is used to handle the * special {@value #ALTERNATIVE} in a path element. * </p> */ private static class OrNode extends Node { final LinkedList<Node> delegates = new LinkedList<Node>(); @Override boolean isMatching( final InstanceContextBean instance ) { boolean matching = false; for( Iterator<Node> it = this.delegates.iterator(); it.hasNext() && ! matching; ) matching = it.next().isMatching( instance ); return matching; } } /** * A node that only matches instances of a given type. */ private static class TypeNode extends Node { final String typeName; /** * Constructor. * @param typeName the type name */ TypeNode( String typeName ) { this.typeName = typeName; } @Override boolean isMatching( InstanceContextBean instance ) { // Find the exact reference. boolean matching = instance.getTypes().contains( this.typeName ); // Or maybe it is a pattern... if( ! matching && this.typeName.contains( JOKER )) { String pattern = this.typeName.replace( JOKER, ".*" ); for( String type : instance.getTypes()) { if( type.matches( pattern )) { matching = true; break; } } } return matching; } } /** * A node that matches instances based on their parent. */ private static class ParentInstanceNode extends Node { final Node parentInstanceNode; /** * Constructor. * @param parentInstanceNode the node that test the parent instances. */ ParentInstanceNode( final Node parentInstanceNode ) { this.parentInstanceNode = parentInstanceNode; } @Override boolean isMatching( final InstanceContextBean instance ) { return instance.getParent() != null && this.parentInstanceNode.isMatching(instance.getParent()); } } /** * A node that only matches root instances. */ private static class RootInstanceNode extends Node { @Override boolean isMatching( final InstanceContextBean instance ) { return instance.getParent() == null; } } }