/**
* Copyright 2016-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.tooling.core.autocompletion;
import static net.roboconf.core.dsl.ParsingConstants.KEYWORD_IMPORT;
import static net.roboconf.core.dsl.ParsingConstants.KEYWORD_INSTANCE_OF;
import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_INSTANCE_CHANNELS;
import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_INSTANCE_NAME;
import static net.roboconf.tooling.core.TextUtils.isLineBreak;
import static net.roboconf.tooling.core.autocompletion.CompletionUtils.basicProposal;
import static net.roboconf.tooling.core.autocompletion.CompletionUtils.buildProposalsFromMap;
import static net.roboconf.tooling.core.autocompletion.CompletionUtils.findAllTypes;
import static net.roboconf.tooling.core.autocompletion.CompletionUtils.resolveStringDescription;
import static net.roboconf.tooling.core.autocompletion.CompletionUtils.startsWith;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.roboconf.core.model.RuntimeModelIo;
import net.roboconf.core.model.RuntimeModelIo.ApplicationLoadResult;
import net.roboconf.core.model.beans.Component;
import net.roboconf.core.model.helpers.ComponentHelpers;
import net.roboconf.core.utils.Utils;
import net.roboconf.tooling.core.SelectionRange;
import net.roboconf.tooling.core.TextUtils;
import net.roboconf.tooling.core.autocompletion.CompletionUtils.RoboconfTypeBean;
/**
* @author Vincent Zurczak - Linagora
*/
public class InstancesCompletionProposer implements ICompletionProposer {
static final String COMPONENT_NAME = "component";
static final String IMPORT_PREFIX = KEYWORD_IMPORT + " ";
static final String INSTANCE_OF_PREFIX = KEYWORD_INSTANCE_OF + " ";
static final String INSTANCE_OF_BLOCK = "New instance block";
private String errorMsg;
private final File appDirectory;
private final File editedFile;
/**
* Constructor.
* @param appDirectory
* @param editedFile
*/
public InstancesCompletionProposer( File appDirectory, File editedFile ) {
this.appDirectory = appDirectory;
this.editedFile = editedFile;
}
/**
* @return a message indicating a potential error met while computing proposals
*/
public String getErrorMsg() {
return this.errorMsg;
}
@Override
public List<RoboconfCompletionProposal> findProposals( String text ) {
// Find the text to insert
List<RoboconfCompletionProposal> proposals = new ArrayList<> ();
Ctx ctx = findContext( text );
boolean addImport = true;
switch( ctx.kind ) {
case IMPORT:
for( String instanceImport : CompletionUtils.findInstancesFilesToImport( this.appDirectory, this.editedFile, text )) {
if( startsWith( instanceImport, ctx.lastWord ))
proposals.add( basicProposal( instanceImport, "", false ));
}
break;
case ATTRIBUTE:
addImport = false;
Map<String,String> candidates = findExportedVariableNames( ctx );
proposals.addAll( buildProposalsFromMap( candidates, ctx.lastWord ));
// No break statement!
case NEUTRAL:
// Import
if( addImport && startsWith( IMPORT_PREFIX, ctx.lastWord ))
proposals.add( basicProposal( IMPORT_PREFIX, ctx.lastWord, true ));
// Instances
if( startsWith( INSTANCE_OF_PREFIX, ctx.lastWord )) {
// Basic proposal: instance of
proposals.add( basicProposal( INSTANCE_OF_PREFIX, ctx.lastWord, true ));
// More complex proposal: a full block
StringBuilder sb = new StringBuilder( INSTANCE_OF_PREFIX );
sb.append( "component" );
sb.append( " {\n\t" );
sb.append( ctx.parentIndentation );
sb.append( PROPERTY_INSTANCE_NAME );
sb.append( ": name;\n" );
sb.append( ctx.parentIndentation );
sb.append( "}" );
String proposalString = sb.toString();
SelectionRange sel1 = new SelectionRange( KEYWORD_INSTANCE_OF.length() + 1, COMPONENT_NAME.length());
SelectionRange sel2 = new SelectionRange(
sel1.getOffset() + sel1.getLength() + PROPERTY_INSTANCE_NAME.length() + 6 + ctx.parentIndentation.length(),
4 );
RoboconfCompletionProposal proposal = new RoboconfCompletionProposal(
proposalString,
INSTANCE_OF_BLOCK,
INSTANCE_OF_BLOCK + "\n\n" + proposalString.trim(),
ctx.lastWord.length());
proposal.getSelection().add( sel1 );
proposal.getSelection().add( sel2 );
proposals.add( proposal );
}
break;
case COMPONENT_NAME:
candidates = findComponentNames( ctx );
proposals.addAll( buildProposalsFromMap( candidates, ctx.lastWord ));
break;
default:
break;
}
return proposals;
}
/**
* @author Vincent Zurczak - Linagora
*/
private static enum CtxKind {
NEUTRAL, // Default: beginning of the document or within an instance
COMMENT, // We are inside a comment
COMPONENT_NAME, // Right after "instance of"
ATTRIBUTE, // At the beginning or right after a colon or an opening curly bracket
ATTRIBUTE_VALUE, // After a colon
IMPORT, // If we are in a file import
NOTHING; // Eliminate some cases
}
/**
* @author Vincent Zurczak - Linagora
*/
private static class Ctx {
CtxKind kind = CtxKind.NEUTRAL;
String lastWord, parentInstanceType = "", parentIndentation = "";
}
/**
* Finds the context from the given text.
* @param text
* @return a context
*/
private Ctx findContext( String text ) {
// Are we inside a comment?
Ctx ctx = new Ctx();
int n;
for( n = text.length() - 1; n >= 0; n-- ) {
char c = text.charAt( n );
if( isLineBreak( c ))
break;
if( c == '#' ) {
ctx.kind = CtxKind.COMMENT;
return ctx;
}
}
// Simplify the search: remove all the comments.
text = TextUtils.removeComments( text );
// Keep on simplifying the search: remove complete instances
// Since instances can contain other instances (recursivity), we must
// apply the pattern several times, until no more replacement is possible.
int before = -1;
int after = -2;
while( before != after ) {
before = text.length();
text = text.replaceAll( "(?i)(?s)instance\\s+of[^{]*\\{[^{}]*\\}", "" );
after = text.length();
}
// Remove white spaces at the beginning of the string.
text = text.replaceAll( "^(\n|\r\n)+", "" );
// Now, find our context.
String lastWord = null, lastLine = null;
StringBuilder sb = new StringBuilder();
for( n = text.length() - 1; n >= 0 && ctx.kind == CtxKind.NEUTRAL; n-- ) {
char c = text.charAt( n );
// After a "colon" => we are in a property, forget it...
if( c == ':' && lastWord != null ) {
ctx.kind = CtxKind.ATTRIBUTE_VALUE;
break;
}
// White space? We have a our last word.
if( Character.isWhitespace( c ) && lastWord == null )
lastWord = sb.toString();
// Store the last line.
if( isLineBreak( c ) && lastLine == null )
lastLine = sb.toString();
// After a semicolon: we should suggest new attributes
if( c == ';' )
ctx.kind = CtxKind.ATTRIBUTE;
// Same thing after a curly bracket
else if( c == '{' )
ctx.kind = CtxKind.ATTRIBUTE;
else {
sb.insert( 0, c );
String cmp = sb.toString();
if( lastWord != null )
cmp = cmp.substring( 0, cmp.length() - lastWord.length());
cmp = cmp.trim().replaceAll( "\\s{2,}", " " );
if( cmp.equals( KEYWORD_INSTANCE_OF ))
ctx.kind = CtxKind.COMPONENT_NAME;
else if( cmp.equals( KEYWORD_IMPORT )
&& ! sb.toString().matches( ".*" + KEYWORD_IMPORT ))
ctx.kind = CtxKind.IMPORT;
}
}
// Update the context
ctx.lastWord = lastWord == null ? sb.toString() : lastWord;
// If we are in the attribute state, we should verify the last line.
// Case "instance of C " would otherwise indicate
if( lastLine == null )
lastLine = sb.toString();
if( lastLine.matches( "(?i).*\\binstance\\s+of\\s+\\S+\\s+" )) {
ctx.kind = CtxKind.NOTHING;
}
// Find the parent instance (which is the first declared instance at the end of 'text').
Pattern p = Pattern.compile( "(?i)([\\t ]*)instance\\s+of\\s+([^{]+)", Pattern.CASE_INSENSITIVE );
Matcher m = p.matcher( text );
while( m.find()) {
// instance of t| => 'lastWord == t' and 'm.group( 2 ) == t''
// instance of => 'lastWord' is not relevant...
if( ! Objects.equals( ctx.lastWord, m.group( 2 ).trim())) {
ctx.parentIndentation = m.group( 1 );
ctx.parentIndentation += "\t";
ctx.parentInstanceType = m.group( 2 ).trim();
}
}
return ctx;
}
/**
* @param ctx the current context
* @return a non-null list of component names
*/
Map<String,String> findComponentNames( Ctx ctx ) {
Map<String,String> result = new TreeMap<> ();
if( this.appDirectory != null ) {
// Ignore parsing errors, propose the most accurate and possible results
ApplicationLoadResult alr = RuntimeModelIo.loadApplicationFlexibly( this.appDirectory );
BLOCK: if( alr.getApplicationTemplate().getGraphs() != null ) {
// If there is a parent component...
Component parentComponent = ComponentHelpers.findComponent( alr.getApplicationTemplate(), ctx.parentInstanceType );
// ... then find out the right potential children.
Collection<Component> candidates = Collections.emptyList();
if( parentComponent != null )
candidates = ComponentHelpers.findAllChildren( parentComponent );
else if( Utils.isEmptyOrWhitespaces( ctx.parentInstanceType ))
candidates = alr.getApplicationTemplate().getGraphs().getRootComponents();
else
this.errorMsg = "Component " + ctx.parentInstanceType + " does not exist.";
// Retrieve descriptions from raw graph files
if( candidates.isEmpty())
break BLOCK;
Map<String,RoboconfTypeBean> types = findAllTypes( this.appDirectory );
for( Component c : candidates ) {
RoboconfTypeBean type = types.get( c.getName());
result.put( c.getName(), type == null || type.isFacet() ? null : type.getDescription());
}
} else {
this.errorMsg = "The graph contains errors. It could not be parsed.";
}
}
return result;
}
/**
* @param ctx the current context
* @return a non-null list of variable names
*/
Map<String,String> findExportedVariableNames( Ctx ctx ) {
Map<String,String> result = new TreeMap<> ();
result.put( PROPERTY_INSTANCE_NAME + ": ", null );
result.put( PROPERTY_INSTANCE_CHANNELS + ": ", null );
if( this.appDirectory != null ) {
// Ignore parsing errors, propose the most accurate and possible results
ApplicationLoadResult alr = RuntimeModelIo.loadApplicationFlexibly( this.appDirectory );
if( alr.getApplicationTemplate().getGraphs() != null ) {
// If there is a owner component...
Component ownerComponent = ComponentHelpers.findComponent( alr.getApplicationTemplate(), ctx.parentInstanceType );
// ... then find out the exported variables that can be overridden.
if( ownerComponent == null ) {
this.errorMsg = "Component " + ctx.parentInstanceType + " does not exist.";
} else for( Map.Entry<String,String> entry : ComponentHelpers.findAllExportedVariables( ownerComponent ).entrySet()) {
String desc = resolveStringDescription( entry.getKey(), entry.getValue());
result.put( entry.getKey() + ": ", desc );
}
} else {
this.errorMsg = "The graph contains errors. It could not be parsed.";
}
}
return result;
}
}