org.eclipse.jface.text.templates.TemplateTranslator Maven / Gradle / Ivy
The newest version!
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.text.templates;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The template translator translates a string into a template buffer. Regions marked as variables
* are translated into TemplateVariable
s.
*
* The EBNF grammar of a valid string is as follows:
*
* template := (text | escape)*.
* text := character - dollar.
* escape := dollar ('{' variable '}' | dollar).
* dollar := '$'.
* variable := identifier | identifier ':' type.
* type := qualifiedname | qualifiedname '(' arguments ')'.
* arguments := (argument ',')* argument.
* argument := qualifiedname | argumenttext.
* qualifiedname := (identifier '.')* identifier.
* argumenttext := "'" (character - "'" | "'" "'")* "'".
* identifier := javaidentifierpart - "$".
*
* Clients may only replace the createVariable
method of this class.
*
*
* @since 3.0
*/
public class TemplateTranslator {
/**
* Regex pattern for identifier.
* Note: For historic reasons, this pattern allows numbers at the beginning of an identifier.
* @since 3.7
*/
private static final String IDENTIFIER= "(?:[\\p{javaJavaIdentifierPart}&&[^\\$]]++)"; //$NON-NLS-1$
/**
* Regex pattern for qualifiedname
* @since 3.4
*/
private static final String QUALIFIED_NAME= "(?:" + IDENTIFIER + "\\.)*+" + IDENTIFIER; //$NON-NLS-1$ //$NON-NLS-2$
/**
* Regex pattern for argumenttext
* @since 3.4
*/
private static final String ARGUMENT_TEXT= "'(?:(?:'')|(?:[^']))*+'"; //$NON-NLS-1$
/**
* Regex pattern for argument
* @since 3.4
*/
private static final String ARGUMENT= "(?:" + QUALIFIED_NAME + ")|(?:" + ARGUMENT_TEXT + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
/**
* Regex pattern for whitespace
* @since 3.5
*/
private static final String SPACES= "\\s*+"; //$NON-NLS-1$
/**
* Precompiled regex pattern for qualified names.
* @since 3.3
*/
private static final Pattern PARAM_PATTERN= Pattern.compile(ARGUMENT);
/**
* Precompiled regex pattern for valid dollar escapes (dollar literals and variables) and
* (invalid) single dollars.
* @since 3.3
*/
private static final Pattern ESCAPE_PATTERN= Pattern.compile(
"\\$\\$|\\$\\{" + // $$|${ //$NON-NLS-1$
SPACES +
"(" + IDENTIFIER + "?+)" + // variable id group (1) //$NON-NLS-1$ //$NON-NLS-2$
SPACES +
"(?:" + //$NON-NLS-1$
":" + //$NON-NLS-1$
SPACES +
"(" + QUALIFIED_NAME + ")" + // variable type group (2) //$NON-NLS-1$ //$NON-NLS-2$
SPACES +
"(?:" + //$NON-NLS-1$
"\\(" + // ( //$NON-NLS-1$
SPACES +
"((?:(?:" + ARGUMENT + ")" + SPACES + "," + SPACES + ")*+(?:" + ARGUMENT + "))" + // arguments group (3) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
SPACES +
"\\)" + // ) //$NON-NLS-1$
")?" + //$NON-NLS-1$
SPACES +
")?" + //$NON-NLS-1$
"\\}|\\$"); // }|$ //$NON-NLS-1$
/**
* @since 3.3
*/
private final class VariableDescription {
final List fOffsets= new ArrayList<>(5);
final String fName;
TemplateVariableType fType;
VariableDescription(String name, TemplateVariableType type) {
fName= name;
fType= type;
}
void mergeType(TemplateVariableType type) throws TemplateException {
if (type == null)
return;
if (fType == null)
fType= type;
if (!type.equals(fType))
fail(TextTemplateMessages.getFormattedString("TemplateTranslator.error.incompatible.type", fName)); //$NON-NLS-1$
}
}
/** Last translation error. */
private String fErrorMessage;
/**
* Used to ensure compatibility with subclasses overriding
* {@link #createVariable(String, String, int[])}.
* @since 3.3
*/
private TemplateVariableType fCurrentType;
/**
* Returns an error message if an error occurred for the last translation, null
* otherwise.
*
* @return the error message if an error occurred during the most recent translation,
* null
otherwise
*/
public String getErrorMessage() {
return fErrorMessage;
}
/**
* Translates a template to a TemplateBuffer
. null
is returned if
* there was an error. getErrorMessage()
retrieves the associated error message.
*
* @param template the template to translate.
* @return returns the template buffer corresponding to the string
* @see #getErrorMessage()
* @throws TemplateException if translation failed
*/
public TemplateBuffer translate(Template template) throws TemplateException {
return parse(template.getPattern());
}
/**
* Translates a template string to TemplateBuffer
. null
is
* returned if there was an error. getErrorMessage()
retrieves the associated
* error message.
*
* @param string the string to translate.
* @return returns the template buffer corresponding to the string
* @see #getErrorMessage()
* @throws TemplateException if translation failed
*/
public TemplateBuffer translate(String string) throws TemplateException {
return parse(string);
}
/**
* Internal parser.
*
* @param string the string to parse
* @return the parsed TemplateBuffer
* @throws TemplateException if the string does not conform to the template format
*/
private TemplateBuffer parse(String string) throws TemplateException {
fErrorMessage= null;
final StringBuilder buffer= new StringBuilder(string.length());
final Matcher matcher= ESCAPE_PATTERN.matcher(string);
final Map variables= new LinkedHashMap<>();
int complete= 0;
while (matcher.find()) {
// append any verbatim text
buffer.append(string.substring(complete, matcher.start()));
// check the escaped sequence
switch (matcher.group()) {
case "$": //$NON-NLS-1$
fail(TextTemplateMessages.getString("TemplateTranslator.error.incomplete.variable")); //$NON-NLS-1$
break;
case "$$": //$NON-NLS-1$
// escaped $
buffer.append('$');
break;
default:
// parse variable
String name= matcher.group(1);
String typeName= matcher.group(2);
String params= matcher.group(3);
TemplateVariableType type= createType(typeName, params);
updateOrCreateVariable(variables, name, type, buffer.length());
buffer.append(name);
break;
}
complete= matcher.end();
}
// append remaining verbatim text
buffer.append(string.substring(complete));
TemplateVariable[] vars= createVariables(variables);
fixOffsetsAndBuffer(buffer, vars);
return new TemplateBuffer(buffer.toString(), vars);
}
/**
* In cases default value is provided dynamically when instantiating the variable (so not part
* of the {@link VariableDescription}), the buffer would actually contain incorrect text (name instead
* of default value) and offsets can also be inconsistent if variable name and default value have different
* length. This methods fix both buffer and variable offsets to ensure default value is used.
* @param buffer the default template string
* @param vars variables
*/
private void fixOffsetsAndBuffer(StringBuilder buffer, TemplateVariable[] vars) {
SortedMap varsByOffset = new TreeMap<>();
for (TemplateVariable var : vars) {
for (int offset : var.getOffsets()) {
varsByOffset.put(Integer.valueOf(offset), var);
}
}
int totalOffsetDelta = 0;
Map> fixedOffsets = new HashMap<>(vars.length, 1.f);
for (Entry entry : varsByOffset.entrySet()) {
final int initialOffset = entry.getKey().intValue();
TemplateVariable variable = entry.getValue();
final int fixedOffset = initialOffset + totalOffsetDelta;
fixedOffsets.computeIfAbsent(variable, v -> new ArrayList<>(v.getOffsets().length)).add(Integer.valueOf(fixedOffset));
int currentOffsetDelta = variable.getDefaultValue().length() - variable.getName().length();
buffer.replace(fixedOffset, fixedOffset + variable.getName().length(), variable.getDefaultValue());
totalOffsetDelta += currentOffsetDelta;
}
fixedOffsets.forEach((variable, fixs) -> variable.setOffsets(fixs.stream().mapToInt(Integer::valueOf).toArray()));
}
private TemplateVariableType createType(String typeName, String paramString) {
if (typeName == null)
return null;
if (paramString == null)
return new TemplateVariableType(typeName);
final Matcher matcher= PARAM_PATTERN.matcher(paramString);
List params= new ArrayList<>(5);
while (matcher.find()) {
String argument= matcher.group();
if (argument.charAt(0) == '\'') {
// argumentText
argument= argument.substring(1, argument.length() - 1).replace("''", "'"); //$NON-NLS-1$ //$NON-NLS-2$
}
params.add(argument);
}
return new TemplateVariableType(typeName, params.toArray(new String[params.size()]));
}
private void fail(String message) throws TemplateException {
fErrorMessage= message;
throw new TemplateException(message);
}
/**
* If there is no variable named name
, a new variable with the given type, name
* and offset is created. If one exists, the offset is added to the variable and the type is
* merged with the existing type.
*
* @param variables the variables by variable name
* @param name the name of the variable
* @param type the variable type, null
for not defined
* @param offset the buffer offset of the variable
* @return the variable description found or created for that name
* @throws TemplateException if merging the type fails
* @since 3.3
*/
private VariableDescription updateOrCreateVariable(Map variables, String name, TemplateVariableType type, int offset) throws TemplateException {
VariableDescription varDesc= variables.get(name);
if (varDesc == null) {
varDesc= new VariableDescription(name, type);
variables.put(name, varDesc);
} else {
varDesc.mergeType(type);
}
varDesc.fOffsets.add(Integer.valueOf(offset));
return varDesc;
}
/**
* Creates proper {@link TemplateVariable}s from the variable descriptions.
*
* @param variables the variable descriptions by variable name
* @return the corresponding variables
* @since 3.3
*/
private TemplateVariable[] createVariables(Map variables) {
TemplateVariable[] result= new TemplateVariable[variables.size()];
int idx= 0;
for (Iterator it= variables.values().iterator(); it.hasNext(); idx++) {
VariableDescription desc= it.next();
TemplateVariableType type= desc.fType == null ? new TemplateVariableType(desc.fName) : desc.fType;
int[] offsets= new int[desc.fOffsets.size()];
int i= 0;
for (Iterator intIt= desc.fOffsets.iterator(); intIt.hasNext(); i++) {
Integer offset= intIt.next();
offsets[i]= offset.intValue();
}
fCurrentType= type;
/*
* Call the deprecated version of createVariable. When not overridden, it will delegate
* to the new version using fCurrentType.
*/
TemplateVariable var= createVariable(type.getName(), desc.fName, offsets);
result[idx]= var;
}
fCurrentType= null; // avoid dangling reference
return result;
}
/**
* Hook method to create new variables. Subclasses may override to supply their custom variable
* type.
*
* Clients may replace this method.
*
*
* @param type the type of the new variable.
* @param name the name of the new variable.
* @param offsets the offsets where the variable occurs in the template
* @return a new instance of TemplateVariable
* @deprecated as of 3.3 use {@link #createVariable(TemplateVariableType, String, int[])} instead
*/
@Deprecated
protected TemplateVariable createVariable(String type, String name, int[] offsets) {
return createVariable(fCurrentType, name, offsets);
}
/**
* Hook method to create new variables. Subclasses may override to supply their custom variable
* type.
*
* Clients may replace this method.
*
*
* @param type the type of the new variable.
* @param name the name of the new variable.
* @param offsets the offsets where the variable occurs in the template
* @return a new instance of TemplateVariable
* @since 3.3
*/
protected TemplateVariable createVariable(TemplateVariableType type, String name, int[] offsets) {
return new TemplateVariable(type, name, name, offsets);
}
}