Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending variables resolving capabilities #3

Merged
merged 10 commits into from
Oct 18, 2012
54 changes: 54 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so

# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip

# Logs and databases #
######################
*.log
*.sql
*.sqlite

# OS generated files #
######################
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db

*.settings
.settings/*
*/.settings/*
**/.settings
**/*.settings
*/target/*
**/target/*
**/target/**
target/*
target/**
*/bin/*
**/bin/*
*.classpath
*.mymetadata
*.project
**/*.project
.idea/
*.iml
target/
118 changes: 118 additions & 0 deletions src/main/java/org/nnsoft/guice/rocoto/variables/AbstractAppender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2009-2012 The 99 Software Foundation
*
* 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 org.nnsoft.guice.rocoto.variables;

import static java.util.logging.Level.FINEST;
import static java.util.logging.Logger.getLogger;

import java.util.Map;
import java.util.logging.Logger;

/**
* Abstract Appender implementation handling resolving context management.
*/
abstract class AbstractAppender implements Appender
{

/** Logger */
private static final Logger logger = getLogger(AbstractAppender.class.getName());

/** Original chunk to process by this appender */
protected final String chunk;

/**
* Default constructor
*
* @param chunk The chunk this appender has to process.
*/
protected AbstractAppender( String chunk )
{
this.chunk = chunk;
}

/**
* Move provided context to current appender and call {@link #doAppend(StringBuilder, Map, Tree, Parser)} if no recursion has been detected.
*
* @param buffer
* @param configuration
* @param context
*/
public final void append( StringBuilder buffer, Map<String, String> configuration, Tree<Appender> context )
{
// Create context if needed
Tree<Appender> currentContext = context == null ? new Tree<Appender>(this) : context.addLeaf(this);

// Check recursion
if ( currentContext.inAncestors(this) )
{
// For the moment just log a warning, and stop the resolving by appending original chunk
buffer.append(chunk);

logger.warning(new StringBuilder("Recursion detected within variable resolving:\n").append(currentContext.getRoot().toString())
.toString());
}
// Process real appending
else
{
doAppend(buffer, configuration, currentContext);
// Dump some info on resolution if this is a root appender
if ( currentContext.isRoot() && logger.isLoggable(FINEST) )
{
logger.finest(new StringBuilder("Resolving variables:\n").append(currentContext.toString()).toString());
}
}
}

/**
* Begin resolving process with this appender against the provided configuration.
*/
public String resolve( Map<String, String> configuration )
{
StringBuilder buffer = new StringBuilder();
append(buffer, configuration, null);
return buffer.toString();
}

/**
* Append something to the provided buffer for the given configuration.<br>
*
* @param buffer
* @param configuration
* @param passed Resolving context, current element is the appender itself.
*/
protected abstract void doAppend( StringBuilder buffer, Map<String, String> configuration, Tree<Appender> context );

/**
* Abstract to force subclasses to re-implement.
*/
@Override
public abstract boolean equals( Object obj );

/**
* Abstract to force subclasses to re-implement.
*/
@Override
public abstract int hashCode();

/**
* @return original chunk
*/
@Override
public final String toString()
{
return chunk;
}
}
197 changes: 197 additions & 0 deletions src/main/java/org/nnsoft/guice/rocoto/variables/AntStyleParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright 2009-2012 The 99 Software Foundation
*
* 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 org.nnsoft.guice.rocoto.variables;

import static java.text.MessageFormat.format;

import java.util.ArrayList;
import java.util.List;

/**
* Parser implementation to resolve ant-style variables.
*
* <h2>Grammar</h2>
*
* <pre>
* expression := (variable|text)*
* variable := '${' expression '|' expression '}'
* text := // any characters except '${'
* </pre>
*
* <h2>Examples</h2>
* <ul>
* <li>Mixed expression: <code>${foo} and ${bar}</code></li>
* <li>Variable with default value: <code>${foo|bar}</code>, <code>${foo|default value is ${bar}}</code>
* <li>Dynamic variable: <code>${${foo.name}}</code></li>
* <li>Etc. <code>${foo${bar|}|${other|${${fallback.name}}!}}</code></li>
* </ul>
*
* <h3>Note</h3> The parser trim variable key and default value thus <tt>${ foo | default }</tt> is equals to <tt>${foo|default}</tt>.
*
*/
public class AntStyleParser implements Parser
{
/** Grammar constants */
static final String VAR_START = "${";
static final int VAR_START_LEN = VAR_START.length();
static final char VAR_CLOSE = '}';
static final int VAR_CLOSE_LEN = 1;
static final char PIPE_SEPARATOR = '|';
static final int PIPE_SEPARATOR_LEN = 1;

/**
* FIXME: Refactor!
*/
public Appender parse( String pattern ) throws IllegalArgumentException
{
List<Appender> appenders = new ArrayList<Appender>();
int prev = 0;
int pos = 0;
while ((pos = pattern.indexOf(VAR_START, pos)) >= 0)
{
// Add text between beginning/end of last variable
if ( pos > prev )
{
appenders.add(new TextAppender(pattern.substring(prev, pos)));
}

// Move to real variable name beginning
pos += VAR_START_LEN;

// Next close bracket (not necessarily the variable end bracket if
// there is a default value with nested variables
int endVariable = pattern.indexOf(VAR_CLOSE, pos);
if ( endVariable < 0 )
{
throw new IllegalArgumentException(format(
"Syntax error in property value ''{0}'', missing close bracket ''{1}'' for variable beginning at col {2}: ''{3}''", pattern,
VAR_CLOSE, pos - VAR_START_LEN, pattern.substring(pos - VAR_START_LEN)));
}

// Try to skip eventual internal variable here
int nextVariable = pattern.indexOf(VAR_START, pos);
// Just used to throw exception with more accurate message
int lastEndVariable = endVariable;
boolean hasNested = false;
while (nextVariable >= 0 && nextVariable < endVariable)
{
hasNested = true;
endVariable = pattern.indexOf(VAR_CLOSE, endVariable + VAR_CLOSE_LEN);
// Something is badly closed
if ( endVariable < 0 )
{
throw new IllegalArgumentException(format(
"Syntax error in property value ''{0}'', missing close bracket ''{1}'' for variable beginning at col {2}: ''{3}''",
pattern, VAR_CLOSE, nextVariable, pattern.substring(nextVariable, lastEndVariable)));
}
nextVariable = pattern.indexOf(VAR_START, nextVariable + VAR_START_LEN);
lastEndVariable = endVariable;
}
// The chunk to process
final String rawKey = pattern.substring(pos - VAR_START_LEN, endVariable + VAR_CLOSE_LEN);
// Key without variable start and end symbols
final String key = pattern.substring(pos, endVariable);

int pipeIndex = key.indexOf(PIPE_SEPARATOR);

boolean hasKeyVariables = false;
boolean hasDefault = false;
boolean hasDefaultVariables = false;

// There is a pipe
if ( pipeIndex >= 0 )
{
// No nested property detected, simple default part
if ( !hasNested )
{
hasDefault = true;
hasDefaultVariables = false;
}
// There is a pipe and nested variable,
// determine if pipe is for the current variable or a nested key
// variable
else
{
int nextStartKeyVariable = key.indexOf(VAR_START);
hasKeyVariables = pipeIndex > nextStartKeyVariable;
if ( hasKeyVariables )
{
// ff${fdf}|${f}
int nextEndKeyVariable = key.indexOf(VAR_CLOSE, nextStartKeyVariable + VAR_START_LEN);
pipeIndex = key.indexOf(PIPE_SEPARATOR, pipeIndex + PIPE_SEPARATOR_LEN);
while (pipeIndex >= 0 && pipeIndex > nextStartKeyVariable)
{
pipeIndex = key.indexOf(PIPE_SEPARATOR, nextEndKeyVariable + VAR_CLOSE_LEN);
nextStartKeyVariable = key.indexOf(VAR_START, nextStartKeyVariable + VAR_START_LEN);
// No more nested variable
if ( nextStartKeyVariable < 0 )
{
break;
}
nextEndKeyVariable = key.indexOf(VAR_CLOSE, nextEndKeyVariable + VAR_CLOSE_LEN);
if ( nextEndKeyVariable < 0 )
{
throw new IllegalArgumentException(
format("Syntax error in property value ''{0}'', missing close bracket ''{1}'' for variable beginning at col {2}: ''{3}''",
pattern, VAR_CLOSE, nextStartKeyVariable, key.substring(nextStartKeyVariable)));
}
}
}

// nested variables are only for key, current variable does
// not have a default value
if ( pipeIndex >= 0 )
{
hasDefault = true;
hasDefaultVariables = key.indexOf(VAR_START, pipeIndex) >= 0;
}

}
}
// No pipe, there is key variables if nested elements have been
// detected
else
{
hasKeyVariables = hasNested;
}

// Construct variable appenders
String keyPart = null;
String defaultPart = null;
if ( hasDefault )
{
keyPart = key.substring(0, pipeIndex).trim();
defaultPart = key.substring(pipeIndex + PIPE_SEPARATOR_LEN).trim();
} else
{
keyPart = key.trim();
}
// Choose TextAppender when relevant to avoid unecessary parsing when it's clearly not needed
appenders.add(new KeyAppender(this, rawKey, hasKeyVariables ? parse(keyPart) : new TextAppender(keyPart), !hasDefault ? null
: (hasDefaultVariables ? parse(defaultPart) : new TextAppender(defaultPart))));

prev = endVariable + VAR_CLOSE_LEN;
pos = prev;
}

if ( prev < pattern.length() )
{
appenders.add(new TextAppender(pattern.substring(prev)));
}

return appenders.size() == 1 ? appenders.get(0) : new MixinAppender(pattern, appenders);
}
}