/** * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.core.docutils; import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import org.eclipse.core.resources.IPathVariableManager; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.variables.VariablesPlugin; import org.python.pydev.core.IPythonNature; import org.python.pydev.core.IPythonPathNature; import org.python.pydev.core.log.Log; import org.python.pydev.shared_core.string.StringUtils; /** * Implements a part of IStringVariableManager (just the performStringSubstitution methods). */ public class StringSubstitution { private Map<String, String> variableSubstitution = null; public StringSubstitution(IPythonNature nature) { if (nature != null) { try { IPythonPathNature pythonPathNature = nature.getPythonPathNature(); IProject project = nature.getProject(); //note: project can be null when creating a new project and receiving a system nature. variableSubstitution = pythonPathNature.getVariableSubstitution(); try { IPathVariableManager projectPathVarManager = null; try { if (project != null) { projectPathVarManager = project.getPathVariableManager(); } } catch (Throwable e1) { //Ignore: getPathVariableManager not available on earlier Eclipse versions. } String[] pathVarNames = null; if (projectPathVarManager != null) { pathVarNames = projectPathVarManager.getPathVariableNames(); } //The usual path var names are: //ECLIPSE_HOME, PARENT_LOC, WORKSPACE_LOC, PROJECT_LOC //Other possible variables may be defined in General > Workspace > Linked Resources. //We also add PROJECT_DIR_NAME (so, we can define a source folder with /${PROJECT_DIR_NAME} if (project != null && !variableSubstitution.containsKey("PROJECT_DIR_NAME")) { IPath location = project.getFullPath(); if (location != null) { variableSubstitution.put("PROJECT_DIR_NAME", location.lastSegment()); } } if (pathVarNames != null) { URI uri = null; String var = null; String path = null; for (int i = 0; i < pathVarNames.length; i++) { try { var = pathVarNames[i]; uri = projectPathVarManager.getURIValue(var); if (uri != null) { String scheme = uri.getScheme(); if (scheme != null && scheme.equalsIgnoreCase("file")) { path = uri.getPath(); if (path != null && !variableSubstitution.containsKey(var)) { variableSubstitution.put(var, new File(uri).toString()); } } } } catch (Exception e) { Log.log(e); } } } } catch (Throwable e) { Log.log(e); } } catch (Exception e) { Log.log(e); } } } /** * Replaces with all variables (the ones for this class and the ones in the VariablesPlugin) * * * Recursively resolves and replaces all variable references in the given * expression with their corresponding values. Allows the client to control * whether references to undefined variables are reported as an error (i.e. * an exception is thrown). * * @param expression expression referencing variables * @param reportUndefinedVariables whether a reference to an undefined variable * is to be considered an error (i.e. throw an exception) * @return expression with variable references replaced with variable values * @throws CoreException if unable to resolve the value of one or more variables */ public String performStringSubstitution(String expression, boolean reportUndefinedVariables) throws CoreException { VariablesPlugin plugin = VariablesPlugin.getDefault(); expression = performPythonpathStringSubstitution(expression); expression = plugin.getStringVariableManager().performStringSubstitution(expression, reportUndefinedVariables); return expression; } /** * String substitution for the pythonpath does not use the default eclipse string substitution (only variables * defined explicitly in this class) */ public String performPythonpathStringSubstitution(String expression) throws CoreException { if (variableSubstitution != null && variableSubstitution.size() > 0 && expression != null && expression.length() > 0) { //Only throw exception here if the expression = new StringSubstitutionEngine().performStringSubstitution(expression, true, variableSubstitution); } return expression; } /** * Recursively resolves and replaces all variable references in the given * expression with their corresponding values. Reports errors for references * to undefined variables (equivalent to calling * <code>performStringSubstitution(expression, true)</code>). * * @param expression expression referencing variables * @return expression with variable references replaced with variable values * @throws CoreException if unable to resolve the value of one or more variables */ public String performStringSubstitution(String expression) throws CoreException { return performStringSubstitution(expression, true); } /** * Performs string substitution for context and value variables. */ class StringSubstitutionEngine { // delimiters private static final String VARIABLE_START = "${"; //$NON-NLS-1$ private static final char VARIABLE_END = '}'; private static final char VARIABLE_ARG = ':'; // parsing states private static final int SCAN_FOR_START = 0; private static final int SCAN_FOR_END = 1; /** * Resulting string */ private StringBuffer fResult; /** * Whether substitutions were performed */ private boolean fSubs; /** * Stack of variables to resolve */ private Stack<VariableReference> fStack; class VariableReference { // the text inside the variable reference private StringBuffer fText; public VariableReference() { fText = new StringBuffer(); } public void append(String text) { fText.append(text); } public String getText() { return fText.toString(); } } /** * Performs recursive string substitution and returns the resulting string. * * @param expression expression to resolve * @param reportUndefinedVariables whether to report undefined variables as an error * @param variableSubstitution registry of variables * @return the resulting string with all variables recursively * substituted * @exception CoreException if unable to resolve a referenced variable or if a cycle exists * in referenced variables */ public String performStringSubstitution(String expression, boolean resolveVariables, Map<String, String> variableSubstitution) throws CoreException { substitute(expression, resolveVariables, variableSubstitution); List<HashSet<String>> resolvedVariableSets = new ArrayList<HashSet<String>>(); while (fSubs) { HashSet<String> resolved = substitute(fResult.toString(), true, variableSubstitution); for (int i = resolvedVariableSets.size() - 1; i >= 0; i--) { HashSet<String> prevSet = resolvedVariableSets.get(i); if (prevSet.equals(resolved)) { HashSet<String> conflictingSet = new HashSet<String>(); for (; i < resolvedVariableSets.size(); i++) { conflictingSet.addAll(resolvedVariableSets.get(i)); } StringBuffer problemVariableList = new StringBuffer(); for (Iterator<?> it = conflictingSet.iterator(); it.hasNext();) { problemVariableList.append(it.next().toString()); problemVariableList.append(", "); //$NON-NLS-1$ } problemVariableList.setLength(problemVariableList.length() - 2); //truncate the last ", " throw new CoreException(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.REFERENCE_CYCLE_ERROR, StringUtils.format("Cycle error on:", problemVariableList.toString()), null)); } } resolvedVariableSets.add(resolved); } return fResult.toString(); } /** * Makes a substitution pass of the given expression returns a Set of the variables that were resolved in this * pass * * @param expression source expression * @param resolveVariables whether to resolve the value of any variables * @exception CoreException if unable to resolve a variable */ private HashSet<String> substitute(String expression, boolean resolveVariables, Map<String, String> variableSubstitution) throws CoreException { fResult = new StringBuffer(expression.length()); fStack = new Stack<VariableReference>(); fSubs = false; HashSet<String> resolvedVariables = new HashSet<String>(); int pos = 0; int state = SCAN_FOR_START; while (pos < expression.length()) { switch (state) { case SCAN_FOR_START: int start = expression.indexOf(VARIABLE_START, pos); if (start >= 0) { int length = start - pos; // copy non-variable text to the result if (length > 0) { fResult.append(expression.substring(pos, start)); } pos = start + 2; state = SCAN_FOR_END; fStack.push(new VariableReference()); } else { // done - no more variables fResult.append(expression.substring(pos)); pos = expression.length(); } break; case SCAN_FOR_END: // be careful of nested variables start = expression.indexOf(VARIABLE_START, pos); int end = expression.indexOf(VARIABLE_END, pos); if (end < 0) { // variables are not completed VariableReference tos = fStack.peek(); tos.append(expression.substring(pos)); pos = expression.length(); } else { if (start >= 0 && start < end) { // start of a nested variable int length = start - pos; if (length > 0) { VariableReference tos = fStack.peek(); tos.append(expression.substring(pos, start)); } pos = start + 2; fStack.push(new VariableReference()); } else { // end of variable reference VariableReference tos = fStack.pop(); String substring = expression.substring(pos, end); tos.append(substring); resolvedVariables.add(substring); pos = end + 1; String value = resolve(tos, resolveVariables, variableSubstitution); if (value == null) { value = ""; //$NON-NLS-1$ } if (fStack.isEmpty()) { // append to result fResult.append(value); state = SCAN_FOR_START; } else { // append to previous variable tos = fStack.peek(); tos.append(value); } } } break; } } // process incomplete variable references while (!fStack.isEmpty()) { VariableReference tos = fStack.pop(); if (fStack.isEmpty()) { fResult.append(VARIABLE_START); fResult.append(tos.getText()); } else { VariableReference var = fStack.peek(); var.append(VARIABLE_START); var.append(tos.getText()); } } return resolvedVariables; } /** * Resolve and return the value of the given variable reference, * possibly <code>null</code>. * * @param var * @param resolveVariables whether to resolve the variables value or just to validate that this variable is valid * @param variableSubstitution variable registry * @return variable value, possibly <code>null</code> * @exception CoreException if unable to resolve a value */ private String resolve(VariableReference var, boolean resolveVariables, Map<String, String> variableSubstitution) throws CoreException { String text = var.getText(); int pos = text.indexOf(VARIABLE_ARG); String name = null; String arg = null; if (pos > 0) { name = text.substring(0, pos); pos++; if (pos < text.length()) { arg = text.substring(pos); } } else { name = text; } String valueVariable = variableSubstitution.get(name); if (valueVariable == null) { //leave as is return getOriginalVarText(var); } if (arg == null) { if (resolveVariables) { fSubs = true; return valueVariable; } //leave as is return getOriginalVarText(var); } // error - an argument specified for a value variable throw new CoreException(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.INTERNAL_ERROR, "Error substituting: " + name + " var: " + valueVariable, null)); } private String getOriginalVarText(VariableReference var) { StringBuffer res = new StringBuffer(var.getText()); res.insert(0, VARIABLE_START); res.append(VARIABLE_END); return res.toString(); } } }