/** * Copyright 2015-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.doc.generator.internal; import java.io.File; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import net.roboconf.core.Constants; 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.Facet; import net.roboconf.core.model.beans.ImportedVariable; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.comparators.AbstractTypeComparator; import net.roboconf.core.model.comparators.InstanceComparator; 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.Utils; import net.roboconf.doc.generator.DocConstants; import net.roboconf.doc.generator.internal.nls.Messages; import net.roboconf.doc.generator.internal.transformers.AbstractRoboconfTransformer; import net.roboconf.doc.generator.internal.transformers.HierarchicalTransformer; import net.roboconf.doc.generator.internal.transformers.InheritanceTransformer; /** * @author Vincent Zurczak - Linagora */ public abstract class AbstractStructuredRenderer implements IRenderer { protected File outputDirectory; protected ApplicationTemplate applicationTemplate; protected File applicationDirectory; protected Map<String,String> options, typeAnnotations; protected Messages messages; protected String locale; /** * @author Vincent Zurczak - Linagora */ public static enum DiagramType { RUNTIME, HIERARCHY, INHERITANCE; } /** * Constructor. * @param outputDirectory * @param applicationTemplate * @param applicationDirectory * @param typeAnnotations (can be null) */ public AbstractStructuredRenderer( File outputDirectory, ApplicationTemplate applicationTemplate, File applicationDirectory, Map<String,String> typeAnnotations ) { this.outputDirectory = outputDirectory; this.applicationTemplate = applicationTemplate; this.applicationDirectory = applicationDirectory; this.typeAnnotations = typeAnnotations != null ? typeAnnotations : new HashMap<String,String>( 0 ); } /* * (non-Javadoc) * @see net.roboconf.doc.generator.internal.IRenderer * #render(java.util.Map) */ @Override public void render( Map<String,String> options ) throws IOException { // Keep the options this.options = options; // Check the language this.locale = options.get( DocConstants.OPTION_LOCALE ); if( this.locale != null ) this.messages = new Messages( this.locale ); else this.messages = new Messages(); // What to render? if( options.containsKey( DocConstants.OPTION_RECIPE )) renderRecipe(); else renderApplication(); } /** * Renders an applicationTemplate. * @throws IOException */ private void renderApplication() throws IOException { StringBuilder sb = new StringBuilder(); // First pages sb.append( renderDocumentTitle()); sb.append( renderPageBreak()); sb.append( renderParagraph( this.messages.get( "intro" ))); //$NON-NLS-1$ sb.append( renderPageBreak()); sb.append( renderDocumentIndex()); sb.append( renderPageBreak()); sb.append( startTable()); sb.append( addTableLine( this.messages.get( "app.name" ), this.applicationTemplate.getName())); //$NON-NLS-1$ sb.append( addTableLine( this.messages.get( "app.qualifier" ), this.applicationTemplate.getQualifier())); //$NON-NLS-1$ sb.append( endTable()); sb.append( renderApplicationDescription()); sb.append( renderPageBreak()); sb.append( renderSections( new ArrayList<String>( 0 ))); // Render information about components sb.append( renderComponents()); // Render information about initial instances sb.append( renderInstances()); writeFileContent( sb.toString()); } /** * Renders a recipe. * @throws IOException */ private void renderRecipe() throws IOException { StringBuilder sb = new StringBuilder(); // First pages if( ! Constants.GENERATED.equalsIgnoreCase( this.applicationTemplate.getName())) { sb.append( renderDocumentTitle()); sb.append( renderPageBreak()); sb.append( renderParagraph( this.messages.get( "intro" ))); //$NON-NLS-1$ sb.append( renderPageBreak()); sb.append( renderDocumentIndex()); sb.append( renderPageBreak()); sb.append( startTable()); sb.append( addTableLine( this.messages.get( "app.name" ), this.applicationTemplate.getName())); //$NON-NLS-1$ sb.append( addTableLine( this.messages.get( "app.qualifier" ), this.applicationTemplate.getQualifier())); //$NON-NLS-1$ sb.append( endTable()); sb.append( renderApplicationDescription()); sb.append( renderPageBreak()); sb.append( renderSections( new ArrayList<String>( 0 ))); } else { sb.append( renderDocumentIndex()); sb.append( renderPageBreak()); } // Render information about components sb.append( renderComponents()); // Render information about facets sb.append( renderFacets()); writeFileContent( sb.toString()); } protected abstract String renderTitle1( String title ); protected abstract String renderTitle2( String title ); protected abstract String renderTitle3( String title ); protected abstract String renderParagraph( String paragraph ); protected abstract String renderList( Collection<String> listItems ); protected abstract String renderPageBreak(); protected abstract String indent(); protected abstract String startTable(); protected abstract String endTable(); protected abstract String addTableHeader( String... headerEntries ); protected abstract String addTableLine( String... lineEntries ); protected abstract String renderDocumentTitle(); protected abstract String renderDocumentIndex(); protected abstract String renderImage( String componentName, DiagramType type, String relativeImagePath ); protected abstract String applyBoldStyle( String text, String keyword ); protected abstract String applyLink( String text, String linkId ); protected abstract File writeFileContent( String fileContent ) throws IOException; protected abstract StringBuilder startSection( String sectionName ); protected abstract StringBuilder endSection( String sectionName, StringBuilder sb ); protected abstract String renderSections( List<String> sectionNames ); /** * Renders information about the components. * @return a string builder (never null) * @throws IOException */ private StringBuilder renderComponents() throws IOException { StringBuilder sb = new StringBuilder(); sb.append( renderTitle1( this.messages.get( "components" ))); //$NON-NLS-1$ sb.append( renderParagraph( this.messages.get( "components.intro" ))); //$NON-NLS-1$ List<String> sectionNames = new ArrayList<> (); List<Component> allComponents = ComponentHelpers.findAllComponents( this.applicationTemplate ); Collections.sort( allComponents, new AbstractTypeComparator()); for( Component comp : allComponents ) { // Start a new section final String sectionName = DocConstants.SECTION_COMPONENTS + comp.getName(); StringBuilder section = startSection( sectionName ); // Overview section.append( renderTitle2( comp.getName())); section.append( renderTitle3( this.messages.get( "overview" ))); //$NON-NLS-1$ String customInfo = readCustomInformation( this.applicationDirectory, comp.getName(), DocConstants.COMP_SUMMARY ); if( Utils.isEmptyOrWhitespaces( customInfo )) customInfo = this.typeAnnotations.get( comp.getName()); if( ! Utils.isEmptyOrWhitespaces( customInfo )) section.append( renderParagraph( customInfo )); String installerName = Utils.capitalize( ComponentHelpers.findComponentInstaller( comp )); installerName = applyBoldStyle( installerName, installerName ); String msg = MessageFormat.format( this.messages.get( "component.installer" ), installerName ); //$NON-NLS-1$ section.append( renderParagraph( msg )); // Facets Collection<Facet> facets = ComponentHelpers.findAllFacets( comp ); if( ! facets.isEmpty()) { section.append( renderTitle3( this.messages.get( "facets" ))); //$NON-NLS-1$ msg = MessageFormat.format(this.messages.get( "component.inherits.facets" ), comp ); section.append( renderParagraph( msg )); section.append( renderList( ComponentHelpers.extractNames( facets ))); } // Inheritance List<Component> extendedComponents = ComponentHelpers.findAllExtendedComponents( comp ); extendedComponents.remove( comp ); Collection<Component> extendingComponents = ComponentHelpers.findAllExtendingComponents( comp ); if( ! extendedComponents.isEmpty() || ! extendingComponents.isEmpty()) { section.append( renderTitle3( this.messages.get( "inheritance" ))); //$NON-NLS-1$ AbstractRoboconfTransformer transformer = new InheritanceTransformer( comp, comp.getExtendedComponent(), extendingComponents, 4 ); saveImage( comp, DiagramType.INHERITANCE, transformer, section ); } if( ! extendedComponents.isEmpty()) { msg = MessageFormat.format( this.messages.get( "component.inherits.properties" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderListAsLinks( ComponentHelpers.extractNames( extendedComponents ))); } if( ! extendingComponents.isEmpty()) { msg = MessageFormat.format( this.messages.get( "component.is.extended.by" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderListAsLinks( ComponentHelpers.extractNames( extendingComponents ))); } // Exported variables Map<String,String> exportedVariables = ComponentHelpers.findAllExportedVariables( comp ); section.append( renderTitle3( this.messages.get( "exports" ))); //$NON-NLS-1$ if( exportedVariables.isEmpty()) { msg = MessageFormat.format( this.messages.get( "component.no.export" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); } else { msg = MessageFormat.format( this.messages.get( "component.exports" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderList( convertExports( exportedVariables ))); } // Hierarchy section.append( renderTitle3( this.messages.get( "hierarchy" ))); //$NON-NLS-1$ Collection<AbstractType> ancestors = new ArrayList<>(); ancestors.addAll( ComponentHelpers.findAllAncestors( comp )); Set<AbstractType> children = new HashSet<>();; children.addAll( ComponentHelpers.findAllChildren( comp )); // For recipes, ancestors and children should include facets if( this.options.containsKey( DocConstants.OPTION_RECIPE )) { for( AbstractType type : comp.getAncestors()) { if( type instanceof Facet ) { ancestors.add( type ); ancestors.addAll( ComponentHelpers.findAllExtendingFacets((Facet) type)); } } for( AbstractType type : comp.getChildren()) { if( type instanceof Facet ) { children.add( type ); children.addAll( ComponentHelpers.findAllExtendingFacets((Facet) type)); } } } if( ! ancestors.isEmpty() || ! children.isEmpty()) { AbstractRoboconfTransformer transformer = new HierarchicalTransformer( comp, ancestors, children, 4 ); saveImage( comp, DiagramType.HIERARCHY, transformer, section ); } if( ancestors.isEmpty()) { msg = MessageFormat.format( this.messages.get( "component.is.root" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); } else { msg = MessageFormat.format( this.messages.get( "component.over" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderListAsLinks( ComponentHelpers.extractNames( ancestors ))); } if( ! children.isEmpty()) { msg = MessageFormat.format( this.messages.get( "component.children" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderListAsLinks( ComponentHelpers.extractNames( children ))); } // Runtime section.append( renderTitle3( this.messages.get( "runtime" ))); //$NON-NLS-1$ Collection<ImportedVariable> imports = ComponentHelpers.findAllImportedVariables( comp ).values(); if( imports.isEmpty()) { msg = MessageFormat.format( this.messages.get( "component.no.dep" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); } else { msg = MessageFormat.format( this.messages.get( "component.depends.on" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderList( getImportComponents( comp ))); msg = MessageFormat.format( this.messages.get( "component.requires" ), comp ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderList( convertImports( imports ))); } // Extra String s = readCustomInformation( this.applicationDirectory, comp.getName(), DocConstants.COMP_EXTRA ); if( ! Utils.isEmptyOrWhitespaces( s )) { section.append( renderTitle3( this.messages.get( "extra" ))); //$NON-NLS-1$ section.append( renderParagraph( s )); } // End the section section = endSection( sectionName, section ); sb.append( section ); sectionNames.add( sectionName ); } sb.append( renderSections( sectionNames )); return sb; } /** * Renders information about the facets. * @return a string builder (never null) * @throws IOException */ private StringBuilder renderFacets() throws IOException { StringBuilder sb = new StringBuilder(); if( ! this.applicationTemplate.getGraphs().getFacetNameToFacet().isEmpty()) { sb.append( renderTitle1( this.messages.get( "facets" ))); //$NON-NLS-1$ sb.append( renderParagraph( this.messages.get( "facets.intro" ))); //$NON-NLS-1$ List<String> sectionNames = new ArrayList<> (); List<Facet> allFacets = new ArrayList<>( this.applicationTemplate.getGraphs().getFacetNameToFacet().values()); Collections.sort( allFacets, new AbstractTypeComparator()); for( Facet facet : allFacets ) { // Start a new section final String sectionName = DocConstants.SECTION_FACETS + facet.getName(); StringBuilder section = startSection( sectionName ); // Overview section.append( renderTitle2( facet.getName())); String customInfo = readCustomInformation( this.applicationDirectory, facet.getName(), DocConstants.FACET_DETAILS ); if( Utils.isEmptyOrWhitespaces( customInfo )) customInfo = this.typeAnnotations.get( facet.getName()); if( ! Utils.isEmptyOrWhitespaces( customInfo )) { section.append( renderTitle3( this.messages.get( "overview" ))); //$NON-NLS-1$ section.append( renderParagraph( customInfo )); } // Exported variables Map<String,String> exportedVariables = ComponentHelpers.findAllExportedVariables( facet ); section.append( renderTitle3( this.messages.get( "exports" ))); //$NON-NLS-1$ if( exportedVariables.isEmpty()) { String msg = MessageFormat.format( this.messages.get( "facet.no.export" ), facet ); //$NON-NLS-1$ section.append( renderParagraph( msg )); } else { String msg = MessageFormat.format( this.messages.get( "facet.exports" ), facet ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderList( convertExports( exportedVariables ))); } // End the section section = endSection( sectionName, section ); sb.append( section ); sectionNames.add( sectionName ); } sb.append( renderSections( sectionNames )); } return sb; } /** * Renders information about the instances. * @return a string builder (never null) */ private StringBuilder renderInstances() { StringBuilder sb = new StringBuilder(); sb.append( renderTitle1( this.messages.get( "instances" ))); //$NON-NLS-1$ sb.append( renderParagraph( this.messages.get( "instances.intro" ))); //$NON-NLS-1$ if( this.applicationTemplate.getRootInstances().isEmpty()) { sb.append( renderParagraph( this.messages.get( "instances.none" ))); //$NON-NLS-1$ } else { sb.append( renderParagraph( this.messages.get( "instances.sorting" ))); //$NON-NLS-1$ // Split by root instance List<String> sectionNames = new ArrayList<> (); Set<Instance> sortedRootInstances = new TreeSet<>( new InstanceComparator()); sortedRootInstances.addAll( this.applicationTemplate.getRootInstances()); for( Instance inst : sortedRootInstances ) { // Start a section final String sectionName = DocConstants.SECTION_INSTANCES + inst.getName(); StringBuilder section = startSection( sectionName ); // Instances overview section.append( renderTitle2( inst.getName())); section.append( startTable()); section.append( addTableHeader( this.messages.get( "instances.instance" ), //$NON-NLS-1$ this.messages.get( "instances.component" ), //$NON-NLS-1$ this.messages.get( "instances.installer" ))); //$NON-NLS-1$ List<Instance> instances = new ArrayList<> (); instances.addAll( InstanceHelpers.buildHierarchicalList( inst )); Collections.sort( instances, new InstanceComparator()); for( Instance i : instances ) { StringBuilder content = new StringBuilder(); String instancePath = InstanceHelpers.computeInstancePath( i ); for( int j=1; j<InstanceHelpers.countInstances( instancePath ); j++ ) content.insert( 0, indent()); content.append( " " ); //$NON-NLS-1$ content.append( i.getName()); String componentName = i.getComponent().getName(); String link = componentName; if( this.options.containsKey( DocConstants.OPTION_HTML_EXPLODED )) link = "../../" + DocConstants.SECTION_COMPONENTS + componentName; //$NON-NLS-1$ String installer = ComponentHelpers.findComponentInstaller( i.getComponent()); section.append( addTableLine( content.toString(), applyLink( componentName, link ), installer )); } section.append( endTable()); // Additional variables? for( Instance i : instances ) { if( ! i.overriddenExports.isEmpty()) { String name = applyBoldStyle( i.getName(), i.getName()); String msg = MessageFormat.format( this.messages.get( "instances.additional" ), name ); //$NON-NLS-1$ section.append( renderParagraph( msg )); section.append( renderList( convertExports( i.overriddenExports ))); } } // End the section section = endSection( sectionName, section ); sb.append( section ); sectionNames.add( sectionName ); } sb.append( renderSections( sectionNames )); } return sb; } /** * Renders the application's description. * @return a non-null string * @throws IOException if something went wrong */ private Object renderApplicationDescription() throws IOException { // No locale? => Display the application's description. // Otherwise, read app.desc_fr_FR.txt or the required file for another locale. // If it does not exist, return the empty string. String s; if( this.locale == null && ! Utils.isEmptyOrWhitespaces( this.applicationTemplate.getDescription())) s = this.applicationTemplate.getDescription(); else s = readCustomInformation( this.applicationDirectory, DocConstants.APP_DESC_PREFIX, DocConstants.FILE_SUFFIX ); String result = ""; if( ! Utils.isEmptyOrWhitespaces( s )) result = renderParagraph( s ); return result; } /** * Generates and saves an image. * @param comp the component to highlight in the image * @param type the kind of relation to show in the diagram * @param transformer a transformer for the graph generation * @param sb the string builder to append the link to the generated image * @throws IOException if something went wrong */ private void saveImage( final Component comp, DiagramType type, AbstractRoboconfTransformer transformer, StringBuilder sb ) throws IOException { String baseName = comp.getName() + "_" + type; //$NON-NLS-1$ String relativePath = "png/" + baseName + ".png"; //$NON-NLS-1$ //$NON-NLS-2$ if( this.options.containsKey( DocConstants.OPTION_GEN_IMAGES_ONCE )) relativePath = "../" + relativePath; File pngFile = new File( this.outputDirectory, relativePath ).getCanonicalFile(); if( ! pngFile.exists()) { Utils.createDirectory( pngFile.getParentFile()); GraphUtils.writeGraph( pngFile, comp, transformer.getConfiguredLayout(), transformer.getGraph(), transformer.getEdgeShapeTransformer(), this.options ); } sb.append( renderImage( comp.getName(), type, relativePath )); } /** * Reads user-specified information from the project. * @param applicationDirectory the application's directory * @param prefix the prefix name * @param suffix the file's suffix (see the DocConstants interface) * @return the read information, as a string (never null) * @throws IOException if the file could not be read */ private String readCustomInformation( File applicationDirectory, String prefix, String suffix ) throws IOException { // Prepare the file name StringBuilder sb = new StringBuilder(); sb.append( prefix ); if( this.locale != null ) sb.append( "_" + this.locale ); sb.append( suffix ); sb.insert( 0, "/" ); //$NON-NLS-1$ sb.insert( 0, DocConstants.DOC_DIR ); // Handle usual (doc) and Maven (src/main/doc) cases File f = new File( applicationDirectory, sb.toString()); if( ! f.exists()) f = new File( f.getParentFile().getParentFile(), sb.toString()); String result = ""; //$NON-NLS-1$ if( f.exists()) result = Utils.readFileContent( f ); return result; } /** * Converts imports to a human-readable text. * @param importedVariables a non-null set of imported variables * @return a non-null list of string, one entry per import */ private List<String> convertImports( Collection<ImportedVariable> importedVariables ) { List<String> result = new ArrayList<> (); for( ImportedVariable var : importedVariables ) { String componentOrFacet = VariableHelpers.parseVariableName( var.getName()).getKey(); String s = applyLink( var.getName(), componentOrFacet ); s += var.isOptional() ? this.messages.get( "optional" ) : this.messages.get( "required" ); //$NON-NLS-1$ //$NON-NLS-2$ if( var.isExternal()) s += this.messages.get( "external" ); //$NON-NLS-1$ result.add( s ); } return result; } /** * Converts exports to a human-readable text. * @param exports a non-null map of exports * @return a non-null list of string, one entry per exported variable */ private List<String> convertExports( Map<String,String> exports ) { List<String> result = new ArrayList<> (); for( Map.Entry<String,String> entry : exports.entrySet()) { String componentOrFacet = VariableHelpers.parseVariableName( entry.getKey()).getKey(); String s = Utils.isEmptyOrWhitespaces( componentOrFacet ) ? entry.getKey() : applyLink( entry.getKey(), componentOrFacet ); if( ! Utils.isEmptyOrWhitespaces( entry.getValue())) s += MessageFormat.format( this.messages.get( "default" ), entry.getValue()); //$NON-NLS-1$ if( entry.getKey().toLowerCase().endsWith( ".ip" )) //$NON-NLS-1$ s += this.messages.get( "injected" ); //$NON-NLS-1$ result.add( s ); } return result; } /** * Converts component dependencies to a human-readable text. * @param component a component * @return a non-null list of component names that match those this component needs */ private List<String> getImportComponents( Component component ) { List<String> result = new ArrayList<> (); Map<String,Boolean> map = ComponentHelpers.findComponentDependenciesFor( component ); for( Map.Entry<String,Boolean> entry : map.entrySet()) { String s = applyLink( entry.getKey(), entry.getKey()); s += entry.getValue() ? this.messages.get( "optional" ) : this.messages.get( "required" ); //$NON-NLS-1$ //$NON-NLS-2$ result.add( s ); } return result; } /** * Renders a list as a list of links. * @param names a list of items * @return a list of links that wrap the names */ private String renderListAsLinks( List<String> names ) { List<String> newNames = new ArrayList<> (); for( String s : names ) newNames.add( applyLink( s, s )); return renderList( newNames ); } }