/*
* This file is part of the OSMembrane project.
* More informations under www.osmembrane.de
*
* The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0.
* for more details about the license see http://www.osmembrane.de/license/
*
* Source: $HeadURL$ ($Revision$)
* Last changed: $Date$
*/
package de.osmembrane.model.parser;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.osmembrane.model.ModelProxy;
import de.osmembrane.model.parser.ParseException.ErrorType;
import de.osmembrane.model.persistence.PipelinePersistenceObject;
import de.osmembrane.model.pipeline.AbstractConnector;
import de.osmembrane.model.pipeline.AbstractFunction;
import de.osmembrane.model.pipeline.AbstractParameter;
import de.osmembrane.model.pipeline.AbstractPipeline;
import de.osmembrane.model.pipeline.AbstractPipelineSettings;
import de.osmembrane.model.pipeline.ConnectorException;
import de.osmembrane.model.pipeline.ConnectorType;
import de.osmembrane.model.pipeline.Pipeline;
import de.osmembrane.model.pipeline.PipelineSettings;
import de.osmembrane.model.settings.SettingType;
import de.osmembrane.tools.I18N;
/**
* Commandline-parser for osmosis command lines.
*
* @author jakob_jarosch
*/
public class CommandlineParser implements IParser {
/**
* If it is not set, the osmosis path will not be added to the pipeline.
*/
private boolean addOsmosisPath = true;
protected String breaklineSymbol = "<linebreak>";
protected String breaklineCommand = "\n";
protected String quotationSymbol = "\"";
protected String commentSymbol = "<COMMENT>: ";
protected Pattern[] regexCommentPatterns = {};
protected boolean disableComments = false;
protected static final String DEFAULT_KEY = "DEFAULTKEY";
protected static final Pattern PATTERN_TASK = Pattern.compile(
"--([^ ]+)(.*?)((?=--)|$)", Pattern.CASE_INSENSITIVE
| Pattern.MULTILINE);
protected static final Pattern PATTERN_PIPE = Pattern
.compile("^(in|out)pipe\\.([0-9+])$");
protected static final Pattern PATTERN_SPLIT_SPACES_PARAMETER = Pattern
.compile("(inPipe|outPipe)", Pattern.CASE_INSENSITIVE
| Pattern.MULTILINE);
/**
* Returns a following group-matching:<br/>
* <br/>
* 0: whole parameter<br/>
* 1: parameter(format: key='value') or NULL<br/>
* 2: key or NULL<br/>
* 3: value or NULL<br/>
* 4: parameter(format: key="value") or NULL<br/>
* 5: key or NULL<br/>
* 6: value or NULL<br/>
* 7: parameter(format: key=value) or NULL<br/>
* 8: key or NULL<br/>
* 9: value or NULL<br/>
* 10: parameter(format: 'value') or NULL<br/>
* 11: value or NULL<br/>
* 12: parameter(format: "value") or NULL<br/>
* 13: value or NULL<br/>
* 14: parameter(format: value) or NULL<br/>
* 15: value or NULL
*/
protected static final Pattern PATTERN_PARAMETER = Pattern.compile(
/* should match on key='value' */
"(([^= ]+)='([^']+)')|"
/* should match on key="value" */
+ "(([^= ]+)=\"([^\"]+)\")|"
/* should match on key=value */
+ "(([^= ]+)=([^ ]+))|"
/* should match on 'value' */
+ "('([^']+)')|"
/* should match on "value" */
+ "(\"([^\"]+)\")|"
/* should match on value */
+ "(([^ ]+))",
/* case insensitive and multiline matching */
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
@Override
public PipelinePersistenceObject parseString(String input)
throws ParseException {
/** A temporary silent pipeline to check loop freeness */
AbstractPipeline pipeline = new Pipeline(true, true);
/** A map for listing all connections belongs to its function */
Map<String, AbstractFunction> connectionMap = new HashMap<String, AbstractFunction>();
/**
* Map saves for each ConnectorType a function which has such an outPipe
* but none explicit defined in the commandline.
*/
Map<ConnectorType, Queue<AbstractFunction>> openOutConnectors = new HashMap<ConnectorType, Queue<AbstractFunction>>();
for (ConnectorType type : ConnectorType.values()) {
openOutConnectors.put(type, new LinkedList<AbstractFunction>());
}
/* replace all comments */
for (Pattern replacePattern : regexCommentPatterns) {
Matcher match = replacePattern.matcher(input);
input = match.replaceAll("");
}
/* join the commandlines */
input = input.replace(breaklineSymbol, "");
input = input.replace(breaklineCommand, " ");
Matcher taskMatcher = PATTERN_TASK.matcher(input);
/* iterate over all found tasks */
while (taskMatcher.find()) {
String taskName = taskMatcher.group(1).toLowerCase();
String taskParameters = taskMatcher.group(2).trim();
Map<String, String> parameters = new HashMap<String, String>();
Map<Integer, String> inPipes = new HashMap<Integer, String>();
Map<Integer, String> outPipes = new HashMap<Integer, String>();
/* parse all parameters */
Matcher paramMatcher = PATTERN_PARAMETER.matcher(taskParameters);
/* iterate over all parameters */
while (paramMatcher.find()) {
String[] keyValuePair = getParameter(paramMatcher);
/*
* change the key to an empty String, if it NULL. this is a
* default parameter.
*/
if (keyValuePair[0] == null) {
keyValuePair[0] = DEFAULT_KEY;
}
/* try to identify the parameter as an pipe */
Matcher pipeMatcher = PATTERN_PIPE.matcher(keyValuePair[0]
.toLowerCase());
if (pipeMatcher.find()) {
/* found a pipe */
String inOutPipe = pipeMatcher.group(1);
int pipeIndex = Integer.parseInt(pipeMatcher.group(2));
if (inOutPipe.equals("in")) {
inPipes.put(pipeIndex, keyValuePair[1]);
} else {
outPipes.put(pipeIndex, keyValuePair[1]);
}
} else {
/* found a normal parameter */
parameters.put(keyValuePair[0], keyValuePair[1]);
}
}
/*
* find the corresponding function to the task but first of all take
* tee and tee-change, 'cause they have no corresponding functions.
*/
if (taskName.equals("tee") || taskName.equals("tee-change")) {
AbstractFunction function = null;
/* get the count of outPipes defined in the tee-task */
int countOutPipes = 2;
if (parameters.get(DEFAULT_KEY) != null) {
countOutPipes = Integer.parseInt(parameters
.get(DEFAULT_KEY));
} else if (parameters.get("outputCount") != null) {
countOutPipes = Integer.parseInt(parameters
.get("outputCount"));
}
/* try to get the function at the inPipe */
for (Integer pipeId : inPipes.keySet()) {
function = connectionMap.get(inPipes.get(pipeId));
}
/* check if that could be also an implicit function */
if (function == null) {
Queue<AbstractFunction> functions = openOutConnectors
.get((taskName.equals("tee") ? ConnectorType.ENTITY
: ConnectorType.CHANGE));
function = functions.poll();
}
/*
* found whether a explicit function nor an implicit one at the
* inConnector
*/
if (function == null) {
throw new ParseException(ErrorType.UNKNOWN_PIPE_STREAM,
taskName);
}
/* add all specified outPipes to the map */
int countedOutPipes = 0;
for (Integer pipeId : outPipes.keySet()) {
countedOutPipes++;
String pipeName = outPipes.get(pipeId);
if (function != null) {
connectionMap.put(pipeName, function);
}
}
/* now add all unspecified outPipes */
while (countedOutPipes < countOutPipes) {
countedOutPipes++;
Queue<AbstractFunction> functions = openOutConnectors
.get((taskName.equals("tee") ? ConnectorType.ENTITY
: ConnectorType.CHANGE));
functions.add(function);
}
/*
* no tee, try to get a real function
*/
} else {
AbstractFunction function = ModelProxy.getInstance()
.getFunctions()
.getMatchingFunctionForTaskName(taskName);
if (function == null) {
throw new ParseException(ErrorType.UNKNOWN_TASK, taskName);
} else {
pipeline.addFunction(function);
}
/*
* We have to check if the function does contain a parameter
* with "hasSpaces" set, if that is so, we have to parse the
* parameters again.
*/
AbstractParameter spacesParam = null;
for (AbstractParameter param : function.getActiveTask()
.getParameters()) {
if (param.hasSpaces()) {
spacesParam = param;
}
}
if (spacesParam != null) {
String[] results = PATTERN_SPLIT_SPACES_PARAMETER
.split(taskParameters);
spacesParam.setValue(results[0].trim());
if (!spacesParam.isValid()) {
throw new ParseException(
ErrorType.INVALID_PARAMETER_VALUE, taskName,
spacesParam.getName(), results[0].trim());
}
/*
* Okay it seems not to be so, that there is a spaces param,
* do it the normal way.
*/
} else {
/* copy parameters to the function */
for (String key : parameters.keySet()) {
boolean foundKey = false;
for (AbstractParameter parameter : function
.getActiveTask().getParameters()) {
if (parameter.getName().toLowerCase()
.equals(key.toLowerCase())
|| (key.equals(DEFAULT_KEY) && parameter
.isDefaultParameter())) {
parameter.setValue(parameters.get(key));
if (!parameter.isValid()) {
throw new ParseException(
ErrorType.INVALID_PARAMETER_VALUE,
taskName, parameter.getName(),
parameters.get(key));
}
foundKey = true;
}
}
if (!foundKey) {
if (key.equals(DEFAULT_KEY)) {
throw new ParseException(
ErrorType.NO_DEFAULT_PARAMETER_FOUND,
taskName, parameters.get(key));
} else {
throw new ParseException(
ErrorType.UNKNOWN_TASK_FORMAT,
taskName, key);
}
}
}
}
/* add the connections */
for (Integer pipeId : inPipes.keySet()) {
String pipeName = inPipes.get(pipeId);
AbstractFunction outFunction = connectionMap.get(pipeName);
/* check if the inPipe has no counterpart as a outPipe */
if (outFunction == null) {
throw new ParseException(
ErrorType.COUNTERPART_PIPE_MISSING, taskName,
pipeId);
}
try {
outFunction.addConnectionTo(function);
} catch (ConnectorException e) {
String connectionExceptionMessage = I18N.getInstance()
.getString(
"Model.Pipeline.AddConnection."
+ e.getType());
throw new ParseException(
ErrorType.CONNECTION_NOT_PERMITTED, outFunction
.getActiveTask().getName(), function
.getActiveTask().getName(),
connectionExceptionMessage);
}
}
/* find connectors without a explicit definition. */
for (AbstractConnector connector : function.getInConnectors()) {
/*
* check if the connector is one without a explicit defined
* pipe.
*/
if (inPipes.get(connector.getConnectorIndex()) == null) {
Queue<AbstractFunction> functions = openOutConnectors
.get(connector.getType());
AbstractFunction outFunction = functions.poll();
if (outFunction != null) {
try {
outFunction.addConnectionTo(function);
} catch (ConnectorException e) {
String connectionExceptionMessage = I18N
.getInstance().getString(
"Model.Pipeline.AddConnection."
+ e.getType());
throw new ParseException(
ErrorType.CONNECTION_NOT_PERMITTED,
outFunction.getActiveTask().getName(),
function.getActiveTask().getName(),
connectionExceptionMessage);
}
/* remove the openConnector from the map */
functions.remove(function);
}
}
}
/* register all open outPipes */
for (Integer pipeId : outPipes.keySet()) {
String pipeName = outPipes.get(pipeId);
connectionMap.put(pipeName, function);
}
/* find connectors without a explicit definition. */
for (AbstractConnector connector : function.getOutConnectors()) {
/*
* check if the connector is one without a explicit defined
* pipe.
*/
if (outPipes.get(connector.getConnectorIndex()) == null) {
Queue<AbstractFunction> functions = openOutConnectors
.get(connector.getType());
functions.add(function);
}
}
}
}
/* use the pipeline algorithm to arrange the functions */
pipeline.arrangePipeline();
/* create the output List */
List<AbstractFunction> returnList = new ArrayList<AbstractFunction>();
for (AbstractFunction function : pipeline.getFunctions()) {
/* remove the observer of this pipeline (no longer required) */
function.deleteObserver(pipeline);
returnList.add(function);
}
return new PipelinePersistenceObject(returnList, new PipelineSettings());
}
/* ************************* */
/* String-Pipeline Generator */
/* ************************* */
@Override
public String parsePipeline(PipelinePersistenceObject pipelineObject) {
List<AbstractFunction> pipeline = pipelineObject.getFunctions();
AbstractPipelineSettings settings = pipelineObject.getSettings();
/* Queue where functions are stored, that haven't been parsed yet. */
Queue<AbstractFunction> functionQueue = new LinkedList<AbstractFunction>();
/* List with all used functions */
List<AbstractFunction> usedFunctions = new ArrayList<AbstractFunction>();
/* connectorMap which maps to each used out-connector a uniqueId */
Map<AbstractConnector, Integer> connectorMap = new HashMap<AbstractConnector, Integer>();
/* StringBuilder for the String-output. */
StringBuilder builder = new StringBuilder();
/* pipeIndex is the uniqueId for out-connectors */
int pipeIndex = 0;
/* add all functions to the queue */
for (AbstractFunction function : pipeline) {
functionQueue.add(function);
}
/* add the comment header block to the pipeline. */
if (!disableComments) {
builder.append(getCommentSymbol()
+ "OSMembrane auto-generated pipeline for osmosis");
builder.append(getBreaklineCommand());
builder.append(getCommentSymbol() + "Name: " + settings.getName());
builder.append(getBreaklineCommand());
builder.append(getCommentSymbol() + "Date: "
+ new Date().toString());
builder.append(getBreaklineCommand());
builder.append(getCommentSymbol() + "Comment:");
builder.append(getBreaklineCommand());
String[] commentLines = settings.getComment().split("(\\r\\n|\\n)");
for (String line : commentLines) {
builder.append(getCommentSymbol() + line);
builder.append(getBreaklineCommand());
}
builder.append(getBreaklineCommand());
}
/* add the path to the osmosis binary */
if (addOsmosisPath) {
builder.append(quotate((String) ModelProxy.getInstance()
.getSettings().getValue(SettingType.DEFAULT_OSMOSIS_PATH)));
if (pipelineObject.getSettings().getVerbose() > 0) {
builder.append(" -v" + settings.getVerbose());
}
if (pipelineObject.getSettings().getDebug() > 0) {
builder.append(" -d" + settings.getDebug());
}
}
/* do the parsing while a function is in the queue */
while (!functionQueue.isEmpty()) {
AbstractFunction function = functionQueue.poll();
/*
* Check if the function does not have any dependencies on a
* function which has not yet been parsed.
*/
boolean addable = true;
for (AbstractConnector inConnector : function.getInConnectors()) {
for (AbstractConnector sourceConnector : inConnector
.getConnections()) {
AbstractFunction sourceFunction = sourceConnector
.getParent();
if (!usedFunctions.contains(sourceFunction)) {
addable = false;
break;
}
}
if (!addable) {
break;
}
}
/*
* Only do the next steps if the function does _not_ have any
* dependencies.
*/
if (addable) {
usedFunctions.add(function);
appendLineBreak(builder);
/*
* get the shortName and the name from the activeTask in the
* function
*/
String stn = function.getActiveTask().getShortName();
String tn = function.getActiveTask().getName();
/* write the task(-short)-name */
if ((Boolean) ModelProxy
.getInstance()
.getSettings()
.getValue(SettingType.USE_SHORT_TASK_NAMES_IF_AVAILABLE)
&& stn != null) {
builder.append("--" + stn);
} else {
builder.append("--" + tn);
}
/* write all parameters of the task */
for (AbstractParameter parameter : function.getActiveTask()
.getParameters()) {
/*
* Only add a parameter when there is not a default value
* assigned, or settings say that they are needed.
*/
String value = null;
if (parameter.getValue() != null) {
value = parameter.getValue();
} else if (parameter.isDefaultValue()
&& (Boolean) ModelProxy
.getInstance()
.getSettings()
.getValue(
SettingType.EXPORT_PARAMETERS_WITH_DEFAULT_VALUES)) {
value = parameter.getDefaultValue();
}
if (value != null) {
/* look up if it is a parameter with set "hasSpaces" */
if (parameter.hasSpaces()
&& parameter.isDefaultParameter()) {
builder.append(" " + parameter.getValue());
} else {
builder.append(" " + parameter.getName() + "="
+ quotate(parameter.getValue()));
}
}
}
/* write all inConnectors */
for (AbstractConnector connector : function.getInConnectors()) {
for (AbstractConnector otherConnector : connector
.getConnections()) {
/*
* Use the offset to get the right connector of the
* attached --tee to otherConnector.
*/
int offset = getConnectorOffset(connector,
otherConnector);
builder.append(" inPipe."
+ connector.getConnectorIndex() + "="
+ (connectorMap.get(otherConnector) + offset));
}
}
/* Create the out-Connectors and add a tee if needed. */
StringBuilder teeBuilder = new StringBuilder();
for (AbstractConnector connector : function.getOutConnectors()) {
pipeIndex++;
builder.append(" outPipe." + connector.getConnectorIndex()
+ "=" + pipeIndex);
/* Add a tee, 'cause more than one connection is attached. */
if (connector.getConnections().length > 1) {
/*
* add to the index + 1, 'cause the first
* tee-out-connector has function.connector + 1 as pipe
* key.
*/
connectorMap.put(connector, (pipeIndex + 1));
appendLineBreak(teeBuilder);
/* add the correct --tee */
teeBuilder
.append("--"
+ (connector.getType() == ConnectorType.ENTITY ? "tee"
: "change-tee") + " ");
teeBuilder.append(connector.getConnections().length
+ " inPipe.0=" + pipeIndex);
/* add all outPipes to the --tee */
for (int i = 0; i < connector.getConnections().length; i++) {
pipeIndex++;
/*
* append a out-pipe for the tee
* (outPipe.Index=pipeIndex
*/
teeBuilder
.append(" outPipe." + i + "=" + pipeIndex);
}
} else {
connectorMap.put(connector, pipeIndex);
}
}
builder.append(teeBuilder);
} else {
/* function does not full-fill all dependencies, enqueue */
functionQueue.add(function);
}
}
return builder.toString();
}
/**
* Sets if the omsosis path should be added or not
*
* @param addOsmosisPath
* boolean
*/
protected void addOsmosisPath(boolean addOsmosisPath) {
this.addOsmosisPath = addOsmosisPath;
}
/**
* Returns the offset between the first outPipe of otherConnecor to the
* connector. Useful for connection to the correct --tee outPipe.
*
* @param connector
* the connector to be connected to the otherConnectors --tee
* @param otherConnector
* the connector where a -tee has to be added
*
* @return the offset of the outPipe of otherConnector for connector
*/
private int getConnectorOffset(AbstractConnector connector,
AbstractConnector otherConnector) {
AbstractConnector[] connections = otherConnector.getConnections();
for (int i = 0; i < connections.length; i++) {
if (connections[i] == connector) {
return i;
}
}
/* should never! happen! */
throw new RuntimeException(
"Sorry, but can't parse that, found a connection with only a connection in one direction.");
}
/**
* Returns a key-value-pair.
*
* @param paramMatcher
* matched parameter which should be parsed.
* @return String-Array with first entry as key and second one as value
*/
private String[] getParameter(Matcher paramMatcher) {
int[] keyEntries = { 1, 4, 7, 10, 12, 14 };
for (int i : keyEntries) {
if (paramMatcher.group(i) != null) {
/* found the right entry */
String key = null;
String value = null;
if (i < 10) {
key = paramMatcher.group(i + 1).trim();
value = paramMatcher.group(i + 2).trim();
} else {
value = paramMatcher.group(i + 1).trim();
}
return new String[] { key, value };
}
}
return null;
}
/**
* Adds a linebreak to a given {@link StringBuilder}.
*/
private void appendLineBreak(StringBuilder builder) {
builder.append(" ");
builder.append(breaklineSymbol);
builder.append(breaklineCommand);
}
/**
* Adds a quotation to a {@link String} if it is needed.
*
* @return quotated {@link String}
*/
private String quotate(String string) {
if (string.contains(" ")) {
return quotationSymbol + string + quotationSymbol;
} else {
return string;
}
}
/* **************************** */
/* Some getters and setters.... */
/* **************************** */
protected void setBreaklineSymbol(String symbol) {
this.breaklineSymbol = symbol;
}
@Override
public String getBreaklineSymbol() {
return breaklineSymbol;
}
protected void setCommentSymbol(String commentSymbol) {
this.commentSymbol = commentSymbol;
}
@Override
public String getCommentSymbol() {
return commentSymbol;
}
protected void setBreaklineCommand(String breaklineCommand) {
this.breaklineCommand = breaklineCommand;
}
@Override
public String getBreaklineCommand() {
return breaklineCommand;
}
protected void setQuotationSymbol(String symbol) {
this.quotationSymbol = symbol;
}
@Override
public String getQuotationSymbol() {
return quotationSymbol;
}
protected void setRegexCommentPatterns(Pattern[] regexCommentPatterns) {
this.regexCommentPatterns = regexCommentPatterns;
}
@Override
public Pattern[] getRegexCommentPatterns() {
return regexCommentPatterns;
}
protected void disableComments(boolean disableComments) {
this.disableComments = disableComments;
}
}