/**
* 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;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.roboconf.core.Constants;
import net.roboconf.core.ErrorCode;
import net.roboconf.core.RoboconfError;
import net.roboconf.core.autonomic.RuleParser;
import net.roboconf.core.commands.CommandsParser;
import net.roboconf.core.dsl.ParsingModelIo;
import net.roboconf.core.dsl.converters.FromGraphDefinition;
import net.roboconf.core.dsl.converters.FromInstanceDefinition;
import net.roboconf.core.dsl.converters.FromInstances;
import net.roboconf.core.dsl.parsing.FileDefinition;
import net.roboconf.core.model.beans.ApplicationTemplate;
import net.roboconf.core.model.beans.Graphs;
import net.roboconf.core.model.beans.Instance;
import net.roboconf.core.model.helpers.RoboconfErrorHelpers;
import net.roboconf.core.utils.Utils;
/**
* @author Vincent Zurczak - Linagora
*/
public final class RuntimeModelIo {
/**
* Constructor.
*/
private RuntimeModelIo() {
// nothing
}
/**
* Loads an application from a directory.
* <p>
* The directory structure must be the following one:
* </p>
* <ul>
* <li>descriptor</li>
* <li>graph</li>
* <li>instances (optional)</li>
* </ul>
*
* @param projectDirectory the project directory
* @return a load result (never null)
*/
public static ApplicationLoadResult loadApplication( File projectDirectory ) {
ApplicationLoadResult result = new ApplicationLoadResult();
ApplicationTemplate app = new ApplicationTemplate();
result.applicationTemplate = app;
ApplicationTemplateDescriptor appDescriptor = null;
File descDirectory = new File( projectDirectory, Constants.PROJECT_DIR_DESC );
// Read the application descriptor
DESC: if( ! descDirectory.exists()) {
RoboconfError error = new RoboconfError( ErrorCode.PROJ_NO_DESC_DIR );
error.setDetails( "Directory path: " + projectDirectory.getAbsolutePath());
result.loadErrors.add( error );
} else {
File descriptorFile = new File( descDirectory, Constants.PROJECT_FILE_DESCRIPTOR );
if( ! descriptorFile.exists()) {
result.loadErrors.add( new RoboconfError( ErrorCode.PROJ_NO_DESC_FILE ));
break DESC;
}
try {
appDescriptor = ApplicationTemplateDescriptor.load( descriptorFile );
app.setName( appDescriptor.getName());
app.setDescription( appDescriptor.getDescription());
app.setQualifier( appDescriptor.getQualifier());
app.setDslId( appDescriptor.getDslId());
app.setExternalExportsPrefix( appDescriptor.getExternalExportsPrefix());
for( Map.Entry<String,String> entry : appDescriptor.externalExports.entrySet())
app.externalExports.put( entry.getKey(), app.getExternalExportsPrefix() + "." + entry.getValue());
Collection<ModelError> errors = RuntimeModelValidator.validate( appDescriptor );
result.loadErrors.addAll( errors );
} catch( IOException e ) {
RoboconfError error = new RoboconfError( ErrorCode.PROJ_READ_DESC_FILE );
StringBuilder sb = new StringBuilder( "IO exception." );
if( e.getMessage() != null ) {
sb.append( " " );
sb.append( e.getMessage());
}
error.setDetails( sb.toString());
result.loadErrors.add( error );
}
}
return loadApplication( projectDirectory, appDescriptor, result );
}
/**
* Loads an application from a directory.
* <p>
* This method allows to load an application which does not have a descriptor.
* If it has one, it will be read. Otherwise, a default one will be generated.
* This is convenient for reusable recipes.
* </p>
*
* @param projectDirectory the project directory
* @return a load result (never null)
*/
public static ApplicationLoadResult loadApplicationFlexibly( File projectDirectory ) {
File descDirectory = new File( projectDirectory, Constants.PROJECT_DIR_DESC );
ApplicationLoadResult result;
if( descDirectory.exists()) {
result = loadApplication( projectDirectory );
} else {
ApplicationTemplateDescriptor appDescriptor = new ApplicationTemplateDescriptor();
appDescriptor.setName( Constants.GENERATED );
appDescriptor.setDslId( Constants.GENERATED );
appDescriptor.setQualifier( Constants.GENERATED );
ApplicationLoadResult alr = new ApplicationLoadResult();
alr.applicationTemplate = new ApplicationTemplate( Constants.GENERATED ).dslId( Constants.GENERATED ).qualifier( Constants.GENERATED );
File graphDirectory = new File( projectDirectory, Constants.PROJECT_DIR_GRAPH );
File[] graphFiles = graphDirectory.listFiles( new GraphFileFilter());
if( graphFiles != null && graphFiles.length > 0 )
appDescriptor.setGraphEntryPoint( graphFiles[ 0 ].getName());
result = loadApplication( projectDirectory, appDescriptor, alr );
}
return result;
}
/**
* Loads an application from a directory.
* @param projectDirectory the project directory
* @param appDescriptor the application's descriptor
* @param result the result to populate
* @return a load result (never null)
*/
private static ApplicationLoadResult loadApplication(
File projectDirectory,
ApplicationTemplateDescriptor appDescriptor,
ApplicationLoadResult result ) {
ApplicationTemplate app = result.applicationTemplate;
result.applicationTemplate.setDirectory( projectDirectory );
// Load the graph
File graphDirectory = new File( projectDirectory, Constants.PROJECT_DIR_GRAPH );
GRAPH: if( ! graphDirectory.exists()) {
RoboconfError error = new RoboconfError( ErrorCode.PROJ_NO_GRAPH_DIR );
error.setDetails( "Directory path: " + projectDirectory.getAbsolutePath());
result.loadErrors.add( error );
} else if( appDescriptor != null
&& ! Utils.isEmptyOrWhitespaces( appDescriptor.getGraphEntryPoint())) {
File mainGraphFile = new File( graphDirectory, appDescriptor.getGraphEntryPoint());
if( ! mainGraphFile.exists()) {
RoboconfError error = new RoboconfError( ErrorCode.PROJ_MISSING_GRAPH_EP );
error.setDetails( "Expected path: " + mainGraphFile.getAbsolutePath());
result.loadErrors.add( error );
break GRAPH;
}
Graphs graphs = loadGraph( mainGraphFile, graphDirectory, result );
app.setGraphs( graphs );
}
// Load the instances
File instDirectory = new File( projectDirectory, Constants.PROJECT_DIR_INSTANCES );
INST: if( appDescriptor != null && instDirectory.exists()) {
if( app.getGraphs() == null ) {
result.loadErrors.add( new RoboconfError( ErrorCode.CO_GRAPH_COULD_NOT_BE_BUILT ));
break INST;
}
if( Utils.isEmptyOrWhitespaces( appDescriptor.getInstanceEntryPoint()))
break INST;
File mainInstFile = new File( instDirectory, appDescriptor.getInstanceEntryPoint());
InstancesLoadResult ilr = loadInstances( mainInstFile, instDirectory, app.getGraphs(), app.getName());
result.getParsedFiles().addAll( ilr.getParsedFiles());
result.objectToSource.putAll( ilr.getObjectToSource());
result.loadErrors.addAll( ilr.getLoadErrors());
app.getRootInstances().addAll( ilr.getRootInstances());
}
// Commands
File commandsDirectory = new File( projectDirectory, Constants.PROJECT_DIR_COMMANDS );
List<String> commandNames = new ArrayList<> ();
if( app.getGraphs() != null && commandsDirectory.exists()) {
for( File f : Utils.listAllFiles( commandsDirectory )) {
if( ! f.getName().endsWith( Constants.FILE_EXT_COMMANDS )) {
result.loadErrors.add( new RoboconfError( ErrorCode.PROJ_INVALID_COMMAND_EXT ));
} else {
CommandsParser parser = new CommandsParser( app, f );
result.loadErrors.addAll( parser.getParsingErrors());
commandNames.add( f.getName().replace( Constants.FILE_EXT_COMMANDS, "" ));
}
}
}
// Autonomic
File autonomicRulesDirectory = new File( projectDirectory, Constants.PROJECT_DIR_RULES_AUTONOMIC );
if( app.getGraphs() != null && autonomicRulesDirectory.exists()) {
for( File f : Utils.listAllFiles( autonomicRulesDirectory )) {
if( ! f.getName().endsWith( Constants.FILE_EXT_RULE )) {
result.loadErrors.add( new RoboconfError( ErrorCode.PROJ_INVALID_RULE_EXT ));
} else {
// Parsing errors
RuleParser parser = new RuleParser( f );
result.loadErrors.addAll( parser.getParsingErrors());
// Invalid references to commands?
List<String> coll = new ArrayList<>( parser.getRule().getCommandsToInvoke());
coll.removeAll( commandNames );
for( String commandName : coll )
result.loadErrors.add( new RoboconfError( ErrorCode.RULE_UNKNOWN_COMMAND, "Command name: " + commandName ));
}
}
}
// Check for files that are not reachable or not in the right directories
if( projectDirectory.isDirectory()) {
String[] exts = { Constants.FILE_EXT_GRAPH, Constants.FILE_EXT_INSTANCES };
File[] directories = { graphDirectory, instDirectory };
for( int i=0; i<exts.length; i++ ) {
List<File> files = Utils.listAllFiles( projectDirectory, exts[ i ]);
List<File> filesWithInvalidLocation = new ArrayList<> ();
for( File f : files ) {
if( ! Utils.isAncestor( directories[ i ], f )) {
result.loadErrors.add( new ParsingError( ErrorCode.PROJ_INVALID_FILE_LOCATION, f, 1 ));
filesWithInvalidLocation.add( f );
}
}
files.removeAll( result.getParsedFiles());
files.removeAll( filesWithInvalidLocation );
for( File f : files )
result.loadErrors.add( new ParsingError( ErrorCode.PROJ_UNREACHABLE_FILE, f, 1 ));
}
}
// Validate the entire application
if( ! RoboconfErrorHelpers.containsCriticalErrors( result.loadErrors )) {
Collection<ModelError> errors = RuntimeModelValidator.validate( app );
result.loadErrors.addAll( errors );
}
return result;
}
/**
* A bean that stores both the application and loading errors.
* @author Vincent Zurczak - Linagora
*/
public static class ApplicationLoadResult {
ApplicationTemplate applicationTemplate;
final Collection<RoboconfError> loadErrors = new ArrayList<> ();
final Map<Object,SourceReference> objectToSource = new HashMap<> ();
final Set<File> parsedFiles = new HashSet<> ();
final Map<String,String> typeAnnotations = new HashMap<> ();
/**
* @return the application (can be null)
*/
public ApplicationTemplate getApplicationTemplate() {
return this.applicationTemplate;
}
/**
* @return the load errors (never null)
*/
public Collection<RoboconfError> getLoadErrors() {
return this.loadErrors;
}
/**
* @return the objectToSource
*/
public Map<Object,SourceReference> getObjectToSource() {
return this.objectToSource;
}
/**
* @return the files that have been parsed
*/
public Set<File> getParsedFiles() {
return this.parsedFiles;
}
/**
* @return the typeAnnotations
*/
public Map<String,String> getTypeAnnotations() {
return this.typeAnnotations;
}
}
/**
* A bean that stores both root instances and loading errors.
* @author Vincent Zurczak - Linagora
*/
public static class InstancesLoadResult {
Collection<Instance> rootInstances = new ArrayList<> ();
final Collection<RoboconfError> loadErrors = new ArrayList<> ();
final Map<Object,SourceReference> objectToSource = new HashMap<> ();
final Set<File> parsedFiles = new HashSet<> ();
/**
* @return the root instances (never null)
*/
public Collection<Instance> getRootInstances() {
return this.rootInstances;
}
/**
* @return the load errors (never null)
*/
public Collection<RoboconfError> getLoadErrors() {
return this.loadErrors;
}
/**
* @return the objectToSource
*/
public Map<Object,SourceReference> getObjectToSource() {
return this.objectToSource;
}
/**
* @return the files that have been parsed
*/
public Set<File> getParsedFiles() {
return this.parsedFiles;
}
}
/**
* Loads a graph file.
* @param graphFile the graph file
* @param graphDirectory the graph directory
* @param alr the application's load result to complete
* @return the built graph (might be null)
*/
public static Graphs loadGraph( File graphFile, File graphDirectory, ApplicationLoadResult alr ) {
FromGraphDefinition fromDef = new FromGraphDefinition( graphDirectory );
Graphs graph = fromDef.buildGraphs( graphFile );
alr.getParsedFiles().addAll( fromDef.getProcessedImports());
if( ! fromDef.getErrors().isEmpty()) {
alr.loadErrors.addAll( fromDef.getErrors());
} else {
Collection<ModelError> errors = RuntimeModelValidator.validate( graph );
alr.loadErrors.addAll( errors );
errors = RuntimeModelValidator.validate( graph, graphDirectory.getParentFile());
alr.loadErrors.addAll( errors );
alr.objectToSource.putAll( fromDef.getObjectToSource());
alr.typeAnnotations.putAll( fromDef.getTypeAnnotations());
}
return graph;
}
/**
* Loads instances from a file.
* @param instancesFile the file definition of the instances (can have imports)
* @param rootDirectory the root directory that contains instance definitions, used to resolve imports
* @param graph the graph to use to resolve instances
* @param applicationName the application name
* @return a non-null result
*/
public static InstancesLoadResult loadInstances( File instancesFile, File rootDirectory, Graphs graph, String applicationName ) {
InstancesLoadResult result = new InstancesLoadResult();
INST: {
if( ! instancesFile.exists()) {
RoboconfError error = new RoboconfError( ErrorCode.PROJ_MISSING_INSTANCE_EP );
error.setDetails( "Expected path: " + instancesFile.getAbsolutePath());
result.loadErrors.add( error );
break INST;
}
FromInstanceDefinition fromDef = new FromInstanceDefinition( rootDirectory );
Collection<Instance> instances = fromDef.buildInstances( graph, instancesFile );
result.getParsedFiles().addAll( fromDef.getProcessedImports());
if( ! fromDef.getErrors().isEmpty()) {
result.loadErrors.addAll( fromDef.getErrors());
break INST;
}
Collection<ModelError> errors = RuntimeModelValidator.validate( instances );
result.loadErrors.addAll( errors );
result.objectToSource.putAll( fromDef.getObjectToSource());
result.getRootInstances().addAll( instances );
}
for( Instance rootInstance : result.rootInstances )
rootInstance.data.put( Instance.APPLICATION_NAME, applicationName );
return result;
}
/**
* Writes all the instances into a file.
* @param targetFile the file to save
* @param rootInstances the root instances (not null)
* @throws IOException if something went wrong
*/
public static void writeInstances( File targetFile, Collection<Instance> rootInstances ) throws IOException {
FileDefinition def = new FromInstances().buildFileDefinition( rootInstances, targetFile, false, true );
ParsingModelIo.saveRelationsFile( def, false, "\n" );
}
/**
* @author Vincent Zurczak - Linagora
*/
static class GraphFileFilter implements FileFilter {
@Override
public boolean accept( File f ) {
return f.isFile() && f.getName().toLowerCase().endsWith( ".graph" );
}
}
}