/** * Copyright 2014-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; import java.io.File; import java.util.ArrayList; 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 net.roboconf.core.Constants; import net.roboconf.core.ErrorCode; import net.roboconf.core.dsl.ParsingConstants; import net.roboconf.core.model.beans.AbstractType; import net.roboconf.core.model.beans.ApplicationTemplate; import net.roboconf.core.model.beans.Component; import net.roboconf.core.model.beans.ExportedVariable; import net.roboconf.core.model.beans.Facet; import net.roboconf.core.model.beans.Graphs; import net.roboconf.core.model.beans.ImportedVariable; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.helpers.ComponentHelpers; import net.roboconf.core.model.helpers.InstanceHelpers; import net.roboconf.core.model.helpers.VariableHelpers; import net.roboconf.core.utils.ResourceUtils; import net.roboconf.core.utils.Utils; /** * A set of methods to validate runtime model objects. * @author Vincent Zurczak - Linagora */ public final class RuntimeModelValidator { /** * Constructor. */ private RuntimeModelValidator() { // nothing } /** * Validates a component. * <p> * Associated facets, extended components, children and ancestors, * are not validated by this method. * </p> * * @param component a component * @return a non-null list of errors */ public static Collection<ModelError> validate( Component component ) { Collection<ModelError> errors = new ArrayList<> (); // Check the name if( Utils.isEmptyOrWhitespaces( component.getName())) errors.add( new ModelError( ErrorCode.RM_EMPTY_COMPONENT_NAME, component )); else if( ! component.getName().matches( ParsingConstants.PATTERN_FLEX_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_COMPONENT_NAME, component, "Component name: " + component )); else if( component.getName().contains( "." )) errors.add( new ModelError( ErrorCode.RM_DOT_IS_NOT_ALLOWED, component, "Component name: " + component )); // Check the installer String installerName = ComponentHelpers.findComponentInstaller( component ); if( Utils.isEmptyOrWhitespaces( installerName )) errors.add( new ModelError( ErrorCode.RM_EMPTY_COMPONENT_INSTALLER, component, "Component name: " + component )); else if( ! installerName.matches( ParsingConstants.PATTERN_FLEX_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_COMPONENT_INSTALLER, component, "Component name: " + component )); else if( ComponentHelpers.findAllAncestors( component ).isEmpty() && ! Constants.TARGET_INSTALLER.equals( installerName )) errors.add( new ModelError( ErrorCode.RM_ROOT_INSTALLER_MUST_BE_TARGET, component, "Component name: " + component )); // Check the name of exported variables for( ExportedVariable exportedVariable : component.exportedVariables.values()) { String exportedVarName = exportedVariable.getName(); if( Utils.isEmptyOrWhitespaces( exportedVarName )) errors.add( new ModelError( ErrorCode.RM_EMPTY_VARIABLE_NAME, component, "Variable name: " + exportedVarName )); else if( ! exportedVarName.matches( ParsingConstants.PATTERN_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, component, "Variable name: " + exportedVarName )); if( exportedVariable.isRandom()) { if( exportedVariable.getRandomKind() == null ) errors.add( new ModelError( ErrorCode.RM_INVALID_RANDOM_KIND, component, "Unknown kind: " + exportedVariable.getRawKind())); if( exportedVariable.getValue() != null ) errors.add( new ModelError( ErrorCode.RM_NO_VALUE_FOR_RANDOM, component, "Variable name: " + exportedVariable.getName())); } } // A component cannot import variables it exports unless these imports are optional. // This covers cluster uses cases (where an element may want to know where are the similar nodes). Map<String,String> allExportedVariables = ComponentHelpers.findAllExportedVariables( component ); for( ImportedVariable var : ComponentHelpers.findAllImportedVariables( component ).values()) { String varName = var.getName(); String patternForImports = ParsingConstants.PATTERN_ID; patternForImports += "(\\.\\*)?"; if( Utils.isEmptyOrWhitespaces( varName )) errors.add( new ModelError( ErrorCode.RM_EMPTY_VARIABLE_NAME, component, "Variable name: " + varName )); else if( ! varName.matches( patternForImports )) errors.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, component, "Variable name: " + varName )); // If the import is optional... if( var.isOptional()) continue; if( allExportedVariables.containsKey( varName )) errors.add( new ModelError( ErrorCode.RM_COMPONENT_IMPORTS_EXPORTS, component, "Variable name: " + varName )); } // No cycle in inheritance String errorMsg = ComponentHelpers.searchForInheritanceCycle( component ); if( errorMsg != null ) errors.add( new ModelError( ErrorCode.RM_CYCLE_IN_COMPONENTS_INHERITANCE, component, errorMsg )); // Containment Cycles? errorMsg = ComponentHelpers.searchForLoop( component ); if( errorMsg != null && errorMsg.startsWith( component.getName())) errors.add( new ModelError( ErrorCode.RM_CYCLE_IN_COMPONENTS, component, errorMsg )); return errors; } /** * Validates a facet. * <p> * Extended facets, associated components, children and ancestors, * are not validated by this method. * </p> * * @param facet a facet * @return a non-null list of errors */ public static Collection<ModelError> validate( Facet facet ) { // Check the name Collection<ModelError> result = new ArrayList<> (); if( Utils.isEmptyOrWhitespaces( facet.getName())) result.add( new ModelError( ErrorCode.RM_EMPTY_FACET_NAME, facet )); else if( ! facet.getName().matches( ParsingConstants.PATTERN_FLEX_ID )) result.add( new ModelError( ErrorCode.RM_INVALID_FACET_NAME, facet, "Facet name: " + facet )); else if( facet.getName().contains( "." )) result.add( new ModelError( ErrorCode.RM_DOT_IS_NOT_ALLOWED, facet, "Facet name: " + facet )); // Check the name of exported variables for( String exportedVarName : facet.exportedVariables.keySet()) { if( Utils.isEmptyOrWhitespaces( exportedVarName )) result.add( new ModelError( ErrorCode.RM_EMPTY_VARIABLE_NAME, facet, "Variable name: " + exportedVarName )); else if( ! exportedVarName.matches( ParsingConstants.PATTERN_ID )) result.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, facet, "Variable name: " + exportedVarName )); } // Look for cycles in inheritance String errorMsg = ComponentHelpers.searchForInheritanceCycle( facet ); if( errorMsg != null ) result.add( new ModelError( ErrorCode.RM_CYCLE_IN_FACETS_INHERITANCE, facet, errorMsg )); return result; } /** * Validates graph resources. * @param graphs the graph(s) * @param projectDirectory the project's directory * @return a non-null collection of errors */ public static Collection<ModelError> validate( Graphs graphs, File projectDirectory ) { Collection<ModelError> result = new ArrayList<> (); for( Component c : ComponentHelpers.findAllComponents( graphs )) { File componentDirectory = ResourceUtils.findInstanceResourcesDirectory( projectDirectory, c ); if( ! componentDirectory.exists()) { ModelError error = new ModelError( ErrorCode.PROJ_NO_RESOURCE_DIRECTORY, c ); error.setDetails( "Component name: " + c.getName()); result.add( error ); } else if( ComponentHelpers.isTarget( c )) { result.addAll( TargetValidator.parseTargetProperties( projectDirectory, c )); } else { result.addAll( RecipesValidator.validateComponentRecipes( projectDirectory, c )); } } return result; } /** * Validates a graph. * @param graphs a graphs instance * @return a non-null list of errors */ public static Collection<ModelError> validate( Graphs graphs ) { Collection<ModelError> errors = new ArrayList<> (); if( graphs.getRootComponents().isEmpty()) errors.add( new ModelError( ErrorCode.RM_NO_ROOT_COMPONENT, graphs )); for( Component rootComponent : graphs.getRootComponents()) { if( ! ComponentHelpers.findAllAncestors( rootComponent ).isEmpty()) errors.add( new ModelError( ErrorCode.RM_NOT_A_ROOT_COMPONENT, rootComponent, "Component name: " + rootComponent )); } // Validate all the components // Prepare the verification of variable matching Map<String,Boolean> importedVariableNameToExported = new HashMap<> (); Map<String,List<Component>> importedVariableToImporters = new HashMap<> (); for( Component component : ComponentHelpers.findAllComponents( graphs )) { // Basic checks errors.addAll( validate( component )); for( Facet facet : ComponentHelpers.findAllFacets( component )) errors.addAll( validate( facet )); // Process the imported variables for( ImportedVariable var : ComponentHelpers.findAllImportedVariables( component ).values()) { // External are skipped if( var.isExternal()) continue; // Others are verified String importedVariableName = var.getName(); if( ! importedVariableNameToExported.containsKey( importedVariableName )) importedVariableNameToExported.put( importedVariableName, Boolean.FALSE ); List<Component> importers = importedVariableToImporters.get( importedVariableName ); if( importers == null ) importers = new ArrayList<> (); importers.add( component ); importedVariableToImporters.put( importedVariableName, importers ); } // Check ALL the exported variables (inherited, etc) for( String exportedVariableName : ComponentHelpers.findAllExportedVariables( component ).keySet()) { importedVariableNameToExported.put( exportedVariableName, Boolean.TRUE ); // Also add "prefix.*" in the map (for wild cards...) String prefix = VariableHelpers.parseVariableName( exportedVariableName ).getKey(); importedVariableNameToExported.put( prefix + "." + Constants.WILDCARD, Boolean.TRUE ); } } // Intermediate step: deal with facet variables Set<String> facetVariables = new HashSet<> (); for( Facet f : graphs.getFacetNameToFacet().values()) { facetVariables.addAll( f.exportedVariables.keySet()); facetVariables.add( f.getName() + "." + Constants.WILDCARD ); } // Are all the imports and exports resolvable? for( Map.Entry<String,Boolean> entry : importedVariableNameToExported.entrySet()) { // Resolved. Great! if( entry.getValue()) continue; // Maybe it is a facet variable, with no component associated with this facet. // This check is useful for recipes. ErrorCode errorCode = ErrorCode.RM_UNRESOLVABLE_VARIABLE; if( facetVariables.contains( entry.getKey())) errorCode = ErrorCode.RM_UNRESOLVABLE_FACET_VARIABLE; // Add an error about unknown variable for( Component component : importedVariableToImporters.get( entry.getKey())) errors.add( new ModelError( errorCode, component, "Variable name: " + entry.getKey())); } // Do we have orphan facets? for( Facet f : graphs.getFacetNameToFacet().values()) { if( f.getAssociatedComponents().isEmpty()) { if( f.getChildren().isEmpty()) errors.add( new ModelError( ErrorCode.RM_ORPHAN_FACET, f, "Facet name: " + f )); else errors.add( new ModelError( ErrorCode.RM_ORPHAN_FACET_WITH_CHILDREN, f, "Facet name: " + f )); // Unreachable components for( AbstractType t : f.getChildren()) { if( t instanceof Component ) errors.add( new ModelError( ErrorCode.RM_UNREACHABLE_COMPONENT, t, "Component name: " + t )); } } } return errors; } /** * Validates an instance. * @param instance an instance (not null) * @return a non-null list of errors */ public static Collection<ModelError> validate( Instance instance ) { // Check the name Collection<ModelError> errors = new ArrayList<> (); if( Utils.isEmptyOrWhitespaces( instance.getName())) errors.add( new ModelError( ErrorCode.RM_EMPTY_INSTANCE_NAME, instance )); else if( ! instance.getName().matches( ParsingConstants.PATTERN_FLEX_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_INSTANCE_NAME, instance, "Instance name: " + instance.getName())); // Check exports if( instance.getComponent() == null ) errors.add( new ModelError( ErrorCode.RM_EMPTY_INSTANCE_COMPONENT, instance )); // Check that it has a valid parent with respect to the graph if( instance.getComponent() != null ) { ErrorCode errorCode = null; Collection<Component> ancestors = ComponentHelpers.findAllAncestors( instance.getComponent()); if( instance.getParent() == null && ! ancestors.isEmpty()) errorCode = ErrorCode.RM_MISSING_INSTANCE_PARENT; else if( instance.getParent() != null ) { if( ! ancestors.contains( instance.getParent().getComponent()) || ! ComponentHelpers.findAllChildren( instance.getParent().getComponent()).contains( instance.getComponent())) errorCode = ErrorCode.RM_INVALID_INSTANCE_PARENT; } if( errorCode != null ) { StringBuilder sb = new StringBuilder( "One of the following parent was expected: " ); for( Iterator<Component> it = ancestors.iterator(); it.hasNext(); ) { sb.append( it.next().getName()); if( it.hasNext()) sb.append( ", " ); } errors.add( new ModelError( errorCode, instance, sb.toString())); } } // Check overridden exports // 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<> (); Set<String> inheritedVarNames; if( instance.getComponent() != null ) inheritedVarNames = ComponentHelpers.findAllExportedVariables( instance.getComponent()).keySet(); else inheritedVarNames = new HashSet<>( 0 ); for( String inheritedVarName : inheritedVarNames ) { 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()) { // The overridden export is complete: Tomcat.port = ... if( inheritedVarNames.contains( entry.getKey())) continue; // The export is incomplete or does not override anything... Set<String> fullNames = localNameToFullNames.get( entry.getKey()); if( fullNames == null ) { errors.add( new ModelError( ErrorCode.RM_MAGIC_INSTANCE_VARIABLE, instance, "Variable name: " + entry.getKey())); } else if( fullNames.size() > 1 ) { StringBuilder sb = new StringBuilder(); sb.append( "Variable '" ); sb.append( entry.getKey()); sb.append( "' overrides " ); for( Iterator<String> it = fullNames.iterator(); it.hasNext(); ) { sb.append( it.next()); if( it.hasNext()) sb.append( ", " ); } errors.add( new ModelError( ErrorCode.RM_AMBIGUOUS_OVERRIDING, instance, sb.toString())); } } // The graph(s) may define exported variables without values. // So, all the variables an instance exports must have a value, except network ones (ip, ...) and random ones. // READ: it is complicated to determine whether a resolved exported variable is a random one or not. // So, we use a trick. For all the random variables in the graph, we define an overridden export value. // This way, we will not get a warning because of random values. Then, we remove the overridden export. // // This way, only non-random variables will raise errors in the validation. // This solution has the GREAT advantage to also work with inheritance (!!!). // Hack: start ("mock" random variables if no overridden export) randomVariablesTrickForValidation( instance, true ); // Hack: stop for( Map.Entry<String,String> entry : InstanceHelpers.findAllExportedVariables( instance ).entrySet()) { String name = entry.getKey(); String value = entry.getValue(); if( Utils.isEmptyOrWhitespaces( value ) && ! Constants.SPECIFIC_VARIABLE_IP.equalsIgnoreCase( name ) && ! name.toLowerCase().endsWith( "." + Constants.SPECIFIC_VARIABLE_IP )) errors.add( new ModelError( ErrorCode.RM_MISSING_VARIABLE_VALUE, instance, "Variable name: " + name )); } // Hack: start (restore overridden exports) randomVariablesTrickForValidation( instance, false ); // Hack: stop return errors; } /** * Validates a collection of instances. * @param instances a non-null collection of instances * @return a non-null list of errors */ public static Collection<ModelError> validate( Collection<Instance> instances ) { Collection<ModelError> errors = new ArrayList<> (); for( Instance i : instances ) errors.addAll( validate( i )); return errors; } /** * Validates an application. * @param app an application template (not null) * @return a non-null list of errors */ public static Collection<ModelError> validate( ApplicationTemplate app ) { // Name Collection<ModelError> errors = new ArrayList<> (); if( Utils.isEmptyOrWhitespaces( app.getName())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_NAME, app )); else if( ! app.getName().matches( ParsingConstants.PATTERN_APP_NAME )) errors.add( new ModelError( ErrorCode.RM_INVALID_APPLICATION_NAME, app )); if( Utils.isEmptyOrWhitespaces( app.getQualifier())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_QUALIFIER, app )); // Graph validation Map<String,String> allExports; if( app.getGraphs() == null ) { errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_GRAPHS, app )); allExports = new HashMap<>( 0 ); } else { errors.addAll( validate( app.getGraphs())); allExports = ComponentHelpers.findAllExportedVariables( app.getGraphs());; } // External export ID if( ! app.externalExports.isEmpty()) { if( Utils.isEmptyOrWhitespaces( app.getExternalExportsPrefix())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_EXPORT_PREFIX, app )); else if( ! app.getExternalExportsPrefix().matches( ParsingConstants.PATTERN_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_APPLICATION_EXPORT_PREFIX, app )); } // Check external exports Set<String> alreadySeen = new HashSet<> (); for( Map.Entry<String,String> entry : app.externalExports.entrySet()) { if( ! entry.getKey().matches( ParsingConstants.PATTERN_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, app, "Variable name: " + entry.getKey())); if( ! allExports.containsKey( entry.getKey())) errors.add( new ModelError( ErrorCode.RM_INVALID_EXTERNAL_EXPORT, app, "Variable name: " + entry.getKey())); if( ! entry.getValue().matches( ParsingConstants.PATTERN_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, app, "Variable name: " + entry.getValue())); if( alreadySeen.contains( entry.getValue())) errors.add( new ModelError( ErrorCode.RM_ALREADY_DEFINED_EXTERNAL_EXPORT, app, "Variable name: " + entry.getValue())); else alreadySeen.add( entry.getValue()); } // Instances validation errors.addAll( validate( InstanceHelpers.getAllInstances( app ))); return errors; } /** * Validates an application descriptor. * @param descriptor a descriptor * @return a non-null list of errors */ public static Collection<ModelError> validate( ApplicationTemplateDescriptor descriptor ) { Collection<ModelError> errors = new ArrayList<> (); if( Utils.isEmptyOrWhitespaces( descriptor.getName())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_NAME, descriptor )); if( Utils.isEmptyOrWhitespaces( descriptor.getQualifier())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_QUALIFIER, descriptor )); if( Utils.isEmptyOrWhitespaces( descriptor.getDslId())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_DSL_ID, descriptor )); if( Utils.isEmptyOrWhitespaces( descriptor.getGraphEntryPoint())) errors.add( new ModelError( ErrorCode.RM_MISSING_APPLICATION_GEP, descriptor )); if( ! descriptor.invalidExternalExports.isEmpty()) errors.add( new ModelError( ErrorCode.PROJ_INVALID_EXTERNAL_EXPORTS, descriptor )); for( Map.Entry<String,String> entry : descriptor.externalExports.entrySet()) { if( ! entry.getKey().matches( ParsingConstants.PATTERN_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, descriptor, "Variable name: " + entry.getKey())); if( ! entry.getValue().matches( ParsingConstants.PATTERN_ID )) errors.add( new ModelError( ErrorCode.RM_INVALID_VARIABLE_NAME, descriptor, "Variable name: " + entry.getValue())); } return errors; } /** * A trick to validate instances with random variables. * @param instance a non-null instance * @param set true to setup the trick, false to tear it down */ private static void randomVariablesTrickForValidation( Instance instance, boolean set ) { final String trickValue = "@# --- #@"; Map<String,ExportedVariable> exportedVariables = instance.getComponent() != null ? instance.getComponent().exportedVariables : new HashMap<String,ExportedVariable>( 0 ); for( ExportedVariable var : exportedVariables.values()) { if( ! var.isRandom()) continue; String overriddenExport = instance.overriddenExports.get( var.getName()); // Set and no export? => Set it. if( set && overriddenExport == null ) instance.overriddenExports.put( var.getName(), trickValue ); // Unset and trick value? => Unset it. else if( ! set && Objects.equals( trickValue, overriddenExport )) instance.overriddenExports.remove( var.getName()); } } }