/*
* RHQ Management Platform
* Copyright (C) 2005-2010 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.bundle.filetemplate.recipe;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.system.SystemInfoFactory;
import org.rhq.core.template.TemplateEngine;
/**
* Parses the file template recipe.
*
* @author John Mazzitelli
*
*/
public class RecipeParser {
private final static String PROPERTY_DEPLOY_DIR = "rhq.deploy.dir";
/** properties that will be set by the deployment automatically and do not need user definition */
private final static Set<String> IGNORE_PROPERTIES;
private Map<String, RecipeCommand> recipeCommands;
private Pattern replacementVariableDeclarationPattern;
private Pattern replacementVariableNamePattern;
private String systemReplacementVariablePrefix;
private boolean replaceVariables = false;
static {
IGNORE_PROPERTIES = new HashSet<String>();
IGNORE_PROPERTIES.add(PROPERTY_DEPLOY_DIR);
}
public RecipeParser() {
this.recipeCommands = createRecipeCommands();
setupReplacementPatterns();
}
/**
* If the parser should replace replacement variables with their corresponding values (as found in
* the parser context), <code>true</code> is returned. If <code>false</code> then the replacement
* variables remain as is when they are passed to their command processor objects.
*
* @return flag
*/
public boolean isReplaceReplacementVariables() {
return this.replaceVariables;
}
public void setReplaceReplacementVariables(boolean flag) {
this.replaceVariables = flag;
}
public void parseRecipe(RecipeContext context) throws Exception {
context.setParser(this);
try {
BufferedReader recipeReader = new BufferedReader(new StringReader(context.getRecipe()));
String line = recipeReader.readLine();
while (line != null) {
line = line.trim();
if (line.length() > 0 && !line.startsWith("#")) {
// only process lines that aren't blank or aren't comment lines that start with #
parseRecipeCommandLine(context, line);
context.setUnknownRecipe(false); // we've successfully processed at least one line, this must be a file template recipe
}
line = recipeReader.readLine();
}
} finally {
context.setParser(null);
}
return;
}
protected void parseRecipeCommandLine(RecipeContext context, String line) throws Exception {
if (isReplaceReplacementVariables()) {
line = replaceReplacementVariables(context, line);
}
String[] commandLineArray = splitCommandLine(line);
String commandName = commandLineArray[0];
String[] arguments = extractArguments(commandLineArray);
RecipeCommand recipeCommand = this.recipeCommands.get(commandName);
if (recipeCommand == null) {
throw new Exception("Unknown command in recipe [" + commandName + "]");
}
Set<String> replacementVars = getReplacementVariables(line);
if (replacementVars != null) {
context.addReplacementVariables(replacementVars);
}
try {
recipeCommand.parse(this, context, arguments);
} catch (Exception e) {
throw new Exception("Error in recipe, line [" + line + "]", e);
}
return;
}
protected HashMap<String, RecipeCommand> createRecipeCommands() {
HashMap<String, RecipeCommand> commands = new HashMap<String, RecipeCommand>();
RecipeCommand[] knownCommands = new RecipeCommand[] { new ScriptRecipeCommand(), //
new BundleRecipeCommand(), //
new ConfigDefRecipeCommand(), //
new CommandRecipeCommand(), //
new FileRecipeCommand(), //
new RealizeRecipeCommand(), //
new DeployRecipeCommand() //
};
for (RecipeCommand recipeCommand : knownCommands) {
commands.put(recipeCommand.getName(), recipeCommand);
}
return commands;
}
protected Set<String> getReplacementVariables(String cmdLine) {
Set<String> replacementVariables = null;
Matcher matcher = this.replacementVariableDeclarationPattern.matcher(cmdLine);
while (matcher.find()) {
String replacementDeclaration = matcher.group();
Matcher nameMatcher = this.replacementVariableNamePattern.matcher(replacementDeclaration);
if (!nameMatcher.find()) {
throw new IllegalArgumentException("Bad replacement declaration [" + replacementDeclaration + "]");
}
String replacementVariable = nameMatcher.group();
if (!replacementVariable.startsWith(this.systemReplacementVariablePrefix)) {
if (replacementVariables == null) {
replacementVariables = new HashSet<String>(1);
}
replacementVariables.add(replacementVariable);
}
}
if (null != replacementVariables) {
replacementVariables.removeAll(IGNORE_PROPERTIES);
}
return replacementVariables;
}
public String replaceReplacementVariables(RecipeContext context, String input) {
// since our replacement strings may include \ and $ characters avoid the use of
// matcher.appendReplacement and like methods when doing this work.
String result = input;
Configuration replacementValues = context.getReplacementVariableValues();
TemplateEngine templateEngine = null;
Matcher matcher = this.replacementVariableDeclarationPattern.matcher(input);
while (matcher.find()) {
String next = matcher.group();
Matcher nameMatcher = this.replacementVariableNamePattern.matcher(next);
if (nameMatcher.find()) {
String key = nameMatcher.group();
String value = (replacementValues != null) ? replacementValues.getSimpleValue(key, null) : null;
if (value == null) {
// our replacement values don't know how to replace the key, see if our system info template engine can
if (templateEngine == null) {
templateEngine = SystemInfoFactory.fetchTemplateEngine();
}
value = templateEngine.replaceTokens(next);
}
if (value != null) {
result = result.replace(next, value);
}
}
}
return result;
}
protected String[] splitCommandLine(String cmdLine) {
cmdLine = cmdLine.replace('\\', '/');
ByteArrayInputStream in = new ByteArrayInputStream(cmdLine.getBytes());
StreamTokenizer strtok = new StreamTokenizer(new InputStreamReader(in));
List<String> args = new ArrayList<String>();
boolean keep_going = true;
// we don't want to parse numbers and we want ' to be a normal word character
strtok.ordinaryChars('0', '9');
strtok.ordinaryChar('.');
strtok.ordinaryChar('-');
strtok.ordinaryChar('\'');
strtok.wordChars(33, 127);
strtok.quoteChar('\"');
// parse the command line
while (keep_going) {
int nextToken;
try {
nextToken = strtok.nextToken();
} catch (IOException e) {
nextToken = StreamTokenizer.TT_EOF;
}
if (nextToken == java.io.StreamTokenizer.TT_WORD) {
args.add(strtok.sval);
} else if (nextToken == '\"') {
args.add(strtok.sval);
} else if ((nextToken == java.io.StreamTokenizer.TT_EOF) || (nextToken == java.io.StreamTokenizer.TT_EOL)) {
keep_going = false;
}
}
return args.toArray(new String[args.size()]);
}
protected String[] extractArguments(String[] commandLine) {
// strip the first element (the command name) from the array, leaving only the arguments
int newLength = commandLine.length - 1;
String[] argsOnly = new String[newLength];
System.arraycopy(commandLine, 1, argsOnly, 0, newLength);
return argsOnly;
}
private void setupReplacementPatterns() {
// note that we use the same as the core util's template engine
// the native system replacement variable prefix is used by the agent-side fact variable names
this.replacementVariableDeclarationPattern = Pattern.compile("@@\\s*(\\w+\\.?)+\\s*@@");
this.replacementVariableNamePattern = Pattern.compile("(\\w+\\.?)+");
this.systemReplacementVariablePrefix = SystemInfoFactory.TOKEN_PREFIX;
}
}