/** * 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.core.model.helpers; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.roboconf.core.Constants; import net.roboconf.core.model.ModelError; import net.roboconf.core.model.RuntimeModelValidator; import net.roboconf.core.model.beans.AbstractApplication; import net.roboconf.core.model.beans.Application; import net.roboconf.core.model.beans.ApplicationTemplate; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.utils.Utils; /** * Helpers related to instances. * @author Vincent Zurczak - Linagora */ public final class InstanceHelpers { /** * Private empty constructor. */ private InstanceHelpers() { // nothing } /** * Builds a string representing the path from a root instance to this instance. * <p> * This string can be considered as a computed ID (or signature) of the instance. * It only makes sense within a given application. * </p> * * @param inst an instance (not null) * @return a string (not null) */ public static String computeInstancePath( Instance inst ) { StringBuilder sb = new StringBuilder(); for( Instance current = inst; current != null; current = current.getParent()) { StringBuilder currentSb = new StringBuilder( "/" ); if( ! Utils.isEmptyOrWhitespaces( current.getName())) currentSb.append( current.getName()); sb.insert( 0, currentSb.toString()); } return sb.toString(); } /** * @param i1 an instance (not null) * @param i2 an instance (not null) * @return true if they have the same path, false otherwise */ public static boolean haveSamePath( Instance i1, Instance i2 ) { return computeInstancePath( i1 ).equals( computeInstancePath( i2 )); } /** * Finds the name of an instance from its path. * @param instancePath a non-null instance path * @return an instance name, or the path itself if it is not valid (e.g. no slash) */ public static String findInstanceName( String instancePath ) { String instanceName = ""; Matcher m = Pattern.compile( "([^/]+)$" ).matcher( instancePath ); if( m.find()) instanceName = m.group( 1 ); return instanceName; } /** * Builds a list of instances ordered hierarchically. * <p> * Basically, we give it a root instance (level 0).<br> * This instance is added in the list head.<br> * Then, it adds child instances at level 1.<br> * Then, at level 2, etc. * </p> * <p> * Last elements in the list are leaves instances. * </p> * * @param inst the instance from which we introspect * <p> * It will be the first item of the resulting list. * </p> * * @return a non-null list */ public static List<Instance> buildHierarchicalList( Instance inst ) { List<Instance> instanceList = new ArrayList<> (); List<Instance> todo = new ArrayList<> (); if( inst != null ) todo.add( inst ); while( ! todo.isEmpty()) { Instance current = todo.remove( 0 ); instanceList.add( current ); todo.addAll( current.getChildren()); } return instanceList; } /** * Inserts a child instance. * <p> * This method does not check anything. * In real implementations, such as in the DM, one should * use {@link #tryToInsertChildInstance(AbstractApplication, Instance, Instance)}. * </p> * * @param child a child instance (not null) * @param parent a parent instance (not null) */ public static void insertChild( Instance parent, Instance child ) { child.setParent( parent ); parent.getChildren().add( child ); } /** * Gets the exported variables for an instance. * <p> * It includes the component variables, and the variables * overridden by the instance. * </p> * * @param instance an instance (not null) * @return a non-null map (key = variable name, value = default variable value - can be null). */ public static Map<String,String> findAllExportedVariables( Instance instance ) { // Find the variables Map<String,String> result = new HashMap<> (); if( instance.getComponent() != null ) result.putAll( ComponentHelpers.findAllExportedVariables( instance.getComponent())); // Overridden variables may not contain the facet or component prefix. // To remain as flexible as possible, we will try to resolve them as component or facet variables. Map<String,Set<String>> localNameToFullNames = new HashMap<> (); for( String inheritedVarName : result.keySet()) { String localName = VariableHelpers.parseVariableName( inheritedVarName ).getValue(); Set<String> fullNames = localNameToFullNames.get( localName ); if( fullNames == null ) fullNames = new HashSet<> (); fullNames.add( inheritedVarName ); localNameToFullNames.put( localName, fullNames ); } for( Map.Entry<String,String> entry : instance.overriddenExports.entrySet()) { Set<String> fullNames = localNameToFullNames.get( entry.getKey()); // No inherited variable => Put it in raw mode. if( fullNames == null ) { result.put( entry.getKey(), entry.getValue()); } // Only one prefix, override the inherited variable else if( fullNames.size() == 1 ) { result.put( fullNames.iterator().next(), entry.getValue()); } // Too many inherited ones? => Put it in raw mode AND override all. // There will be a warning in the validation. See #420 else { result.put( entry.getKey(), entry.getValue()); for( String name : fullNames ) result.put( name, entry.getValue()); } } // Update some values String ip = findRootInstance( instance ).data.get( Instance.IP_ADDRESS ); if( ip != null ) VariableHelpers.updateNetworkVariables( result, ip ); return result; } /** * Finds an instance by name. * @param application the application * @param instancePath the instance path * @return an instance, or null if it was not found */ public static Instance findInstanceByPath( AbstractApplication application, String instancePath ) { Collection<Instance> currentList = new ArrayList<> (); if( application != null ) currentList.addAll( application.getRootInstances()); List<String> instanceNames = new ArrayList<> (); if( instancePath != null ) instanceNames.addAll( Arrays.asList( instancePath.split( "/" ))); if( instanceNames.size() > 0 && Utils.isEmptyOrWhitespaces( instanceNames.get( 0 ))) instanceNames.remove( 0 ); // Every path segment points to an instance Instance result = null; for( String instanceName : instanceNames ) { result = null; for( Instance instance : currentList ) { if( instanceName.equals( instance.getName())) { result = instance; break; } } // The segment does not match any instance if( result == null ) break; // Otherwise, prepare the next iteration currentList = result.getChildren(); } return result; } /** * Finds an instance by name. * @param rootInstance a root instance * @param instancePath the instance path * @return an instance, or null if it was not found */ public static Instance findInstanceByPath( Instance rootInstance, String instancePath ) { Application tempApplication = new Application( new ApplicationTemplate()); if( rootInstance != null ) tempApplication.getRootInstances().add( rootInstance ); return findInstanceByPath( tempApplication, instancePath ); } /** * Finds instances by component name. * @param application an application (not null) * @param componentName a component name (not null) * @return a non-null list of instances */ public static List<Instance> findInstancesByComponentName( AbstractApplication application, String componentName ) { List<Instance> result = new ArrayList<> (); for( Instance inst : getAllInstances( application )) { if( componentName.equals( inst.getComponent().getName())) result.add( inst ); } return result; } /** * Finds the root instance for an instance. * @param instance an instance (not null) * @return a non-null instance, the root instance */ public static Instance findRootInstance( Instance instance ) { Instance rootInstance = instance; while( rootInstance.getParent() != null ) rootInstance = rootInstance.getParent(); return rootInstance; } /** * Finds the scoped instance for an instance (i.e. the agent that manages this instance). * @param instance an instance (not null) * @return a non-null instance, the scoped instance */ public static Instance findScopedInstance( Instance instance ) { Instance scopedInstance = instance; while( scopedInstance.getParent() != null && ! isTarget( scopedInstance )) scopedInstance = scopedInstance.getParent(); return scopedInstance; } /** * Finds all the scoped instances from an application. * @return a non-null list */ public static List<Instance> findAllScopedInstances( AbstractApplication app ) { List<Instance> instanceList = new ArrayList<> (); List<Instance> todo = new ArrayList<> (); todo.addAll( app.getRootInstances()); while( ! todo.isEmpty()) { Instance current = todo.remove( 0 ); todo.addAll( current.getChildren()); if( isTarget( current )) instanceList.add( current ); } return instanceList; } /** * Gets all the instances of an application. * @param application an application (not null) * @return a non-null list of instances * <p> * The result is a list made up of ordered lists.<br> * root-0, child-0-1, child-0-2, child-0-1-1, etc.<br> * root-1, child-1-1, child-1-2, child-1-1-1, etc.<br> * </p> * <p> * It means the resulting list can be considered as valid for starting instances * from the root instances to the bottom leaves. * </p> */ public static List<Instance> getAllInstances( AbstractApplication application ) { List<Instance> result = new ArrayList<> (); for( Instance instance : application.getRootInstances()) result.addAll( InstanceHelpers.buildHierarchicalList( instance )); return result; } /** * Tries to insert a child instance. * <ol> * <li>Check if there is no child instance with this name.</li> * <li>Check that the graph(s) allow it (coherence with respect to the components).</li> * <li>Insert the instance.</li> * <li>Validate the application after insertion.</li> * <li>Critical error => revert the insertion.</li> * </ol> * <p> * This method assumes the application is already valid before the insertion. * Which makes sense. * </p> * * @param application the application (not null) * @param parentInstance the parent instance (can be null) * @param childInstance the child instance (not null) * @return true if the child instance could be inserted, false otherwise */ public static boolean tryToInsertChildInstance( AbstractApplication application, Instance parentInstance, Instance childInstance ) { // First, make sure there is no child instance with this name before inserting. // Otherwise, removing the child instance may result randomly. boolean hasAlreadyAChildWithThisName = hasChildWithThisName( application, parentInstance, childInstance.getName()); // We insert a "root instance" boolean success = false; if( parentInstance == null ) { if( ! hasAlreadyAChildWithThisName && ComponentHelpers.findAllAncestors( childInstance.getComponent()).isEmpty()) { application.getRootInstances().add( childInstance ); success = true; // No validation here, but maybe we should... } } // We insert a child instance else { if( ! hasAlreadyAChildWithThisName && ComponentHelpers.findAllChildren( parentInstance.getComponent()).contains( childInstance.getComponent())) { InstanceHelpers.insertChild( parentInstance, childInstance ); Collection<Instance> allInstances = InstanceHelpers.getAllInstances( application ); Collection<ModelError> errors = RuntimeModelValidator.validate( allInstances ); if( RoboconfErrorHelpers.containsCriticalErrors( errors )) { parentInstance.getChildren().remove( childInstance ); childInstance.setParent( null ); } else { success = true; } } } return success; } /** * Determines whether an instance name is not already used by a sibling instance. * @param application the application (not null) * @param parentInstance the parent instance (can be null to indicate a root instance) * @param nameToSearch the name to search * @return true if a child instance of <code>parentInstance</code> has the same name, false otherwise */ public static boolean hasChildWithThisName( AbstractApplication application, Instance parentInstance, String nameToSearch ) { boolean hasAlreadyAChildWithThisName = false; Collection<Instance> list = parentInstance == null ? application.getRootInstances() : parentInstance.getChildren(); for( Iterator<Instance> it = list.iterator(); it.hasNext() && ! hasAlreadyAChildWithThisName; ) { hasAlreadyAChildWithThisName = Objects.equals( nameToSearch, it.next().getName()); } return hasAlreadyAChildWithThisName; } /** * Finds the directory where an agent stores the files for a given instance. * @param instance an instance (not null) * @return a file (not null, but may not exist) */ public static File findInstanceDirectoryOnAgent( Instance instance ) { String path = InstanceHelpers.computeInstancePath( instance ); path = path.substring( 1 ).replace( '/', '_' ).replace( ' ', '_' ); String installerName = ComponentHelpers.findComponentInstaller( instance.getComponent()); return new File( Constants.WORK_DIRECTORY_AGENT, installerName + "/" + path ); } /** * Count the number of instance names into a given path. * @param instancePath an instance path (not null) * @return an integer */ public static int countInstances( String instancePath ) { return instancePath.split( "/" ).length - 1; } /** * Replicates an instance and its children. * <p> * The result does not have any parent. It does not have any * data and imports. In fact, only the name, * the component association, the channel and the overridden exports * are copied. The children are not "referenced" but replicated. * </p> * * @param instance a non-null instance to replicate * @return a non-null instance */ public static Instance replicateInstance( Instance instance ) { Map<Instance,Instance> instanceToDuplicate = new HashMap<> (); List<Instance> toProcess = new ArrayList<> (); toProcess.add( instance ); while( ! toProcess.isEmpty()) { Instance current = toProcess.remove( 0 ); Instance copy = new Instance(); copy.name( current.getName()); copy.component( current.getComponent()); copy.channels.addAll( current.channels ); copy.overriddenExports.putAll( current.overriddenExports ); instanceToDuplicate.put( current, copy ); Instance parent = instanceToDuplicate.get( current.getParent()); if( parent != null ) insertChild( parent, copy ); toProcess.addAll( current.getChildren()); } return instanceToDuplicate.get( instance ); } /** * Determines whether an instances is associated with the "target" installer or not. * @param instance an instance (not null) * @return true if it is associated with the "target" installer, false otherwise */ public static boolean isTarget( Instance instance ) { return instance.getComponent() != null && ComponentHelpers.isTarget( instance.getComponent()); } /** * Removes children instances which are off-scope. * <p> * Agents manage all the instances under their scoped instance, except those * that are associated with the "target" installer. Such instances are indeed managed by another agent. * </p> * * @param scopedInstance a scoped instance */ public static void removeOffScopeInstances( Instance scopedInstance ) { List<Instance> todo = new ArrayList<> (); todo.addAll( scopedInstance.getChildren()); while( ! todo.isEmpty()) { Instance current = todo.remove( 0 ); if( isTarget( current )) current.getParent().getChildren().remove( current ); else todo.addAll( current.getChildren()); } } /** * Finds the root instance's path from another instance path. * @param scopedInstancePath a scoped instance path (may be null) * @return a non-null root instance path */ public static String findRootInstancePath( String scopedInstancePath ) { String result; if( Utils.isEmptyOrWhitespaces( scopedInstancePath )) { // Do not return null result = ""; } else if( scopedInstancePath.contains( "/" )) { // Be as flexible as possible with paths String s = scopedInstancePath.replaceFirst( "^/*", "" ); int index = s.indexOf( '/' ); result = index > 0 ? s.substring( 0, index ) : s; } else { // Assumed to be a root instance name result = scopedInstancePath; } return result; } /** * Fixes overridden exports. * <p> * The given instance is supposed to have a correct component, but may have * duplicates in overridden exports (e.g. the same exports as in the component, left * unchanged). This method will fix it. * </p> * @param instance the instance to fix */ public static void fixOverriddenExports( Instance instance ) { if( ! instance.overriddenExports.isEmpty()) { Map<String,String> componentExports = ComponentHelpers.findAllExportedVariables( instance.getComponent()); Iterator<Map.Entry<String,String>> iter = instance.overriddenExports.entrySet().iterator(); while( iter.hasNext()) { // Does export exist in component... with the same value as in overridden exports? // If so, remove from overridden exports (because it is not overridden at all !) Map.Entry<String,String> entry = iter.next(); String value = componentExports.get( entry.getKey()); if( Objects.equals( value, entry.getValue())) iter.remove(); } } } }