properties loaded from plugin

This commit is contained in:
nbaars 2015-01-10 10:12:08 +01:00
parent 3d8a345264
commit 3d6236242f
13 changed files with 165 additions and 128 deletions

View File

@ -5,98 +5,104 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.nio.file.StandardOpenOption;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.owasp.webgoat.plugins.PluginFileUtils.fileEndsWith;
import static org.owasp.webgoat.plugins.PluginFileUtils.hasParentDirectoryWithName;
public class Plugin { public class Plugin {
private static final Logger logger = LoggerFactory.getLogger(Plugin.class); private static final String NAME_LESSON_SOLUTION_DIRECTORY = "lessonSolutions";
private final Class<AbstractLesson> lesson; private static final String NAME_LESSON_PLANS_DIRECTORY = "lessonPlans";
private static final String NAME_LESSON_I18N_DIRECTORY = "i18n";
private final Logger logger = LoggerFactory.getLogger(Plugin.class);
private final Path pluginDirectory; private final Path pluginDirectory;
private final Map<String, File> solutionLanguageFiles;
private final Map<String, File> lessonPlansLanguageFiles; private Class<AbstractLesson> lesson;
private final File lessonSourceFile; private Map<String, File> solutionLanguageFiles = new HashMap<>();
private Map<String, File> lessonPlansLanguageFiles = new HashMap<>();
private File lessonSourceFile;
public static class PluginLoadingFailure extends RuntimeException { public static class PluginLoadingFailure extends RuntimeException {
public PluginLoadingFailure(String message) { public PluginLoadingFailure(String message) {
super(message); super(message);
} }
}
public static class Builder { public PluginLoadingFailure(String message, Exception e) {
super(message, e);
private Path pluginDirectory;
private Class lesson;
private final List<String> loadedClasses = new ArrayList<String>();
private final Map<String, File> solutionLanguageFiles = new HashMap<>();
private final Map<String, File> lessonPlansLanguageFiles = new HashMap<>();
private File javaSource;
public Builder loadClasses(Map<String, byte[]> classes) {
for (Map.Entry<String, byte[]> clazz : classes.entrySet() ) {
loadClass(clazz.getKey(), clazz.getValue());
}
return this;
}
public Builder loadClass(String name, byte[] classFile) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
PluginClassLoader pluginClassLoader = new PluginClassLoader(contextClassLoader, classFile);
try {
String realClassName = name.replaceFirst("/", "").replaceAll("/", ".").replaceAll(".class", "");
Class clazz = pluginClassLoader.loadClass(realClassName);
if (AbstractLesson.class.isAssignableFrom(clazz)) {
this.lesson = clazz;
}
loadedClasses.add(clazz.getName());
} catch (ClassNotFoundException e) {
logger.error("Unable to load class {}", name);
}
return this;
}
public Builder setBaseDirectory(Path pluginDirectory) {
this.pluginDirectory = pluginDirectory;
return this;
}
public Plugin build() {
if ( lesson == null ) {
throw new PluginLoadingFailure(String.format("Lesson class not found, following classes were detected in the plugin: %s",
StringUtils.collectionToCommaDelimitedString(loadedClasses)));
}
return new Plugin(this.lesson, pluginDirectory, lessonPlansLanguageFiles, solutionLanguageFiles, javaSource);
}
public void loadFiles(List<Path> files) {
for (Path file : files) {
if (file.getFileName().toString().endsWith(".html") && file.getParent().getParent().getFileName().toString()
.endsWith("lessonSolutions")) {
solutionLanguageFiles.put(file.getParent().getFileName().toString(), file.toFile());
}
if (file.getFileName().toString().endsWith(".html") && file.getParent().getParent().getFileName().toString()
.endsWith("lessonPlans")) {
lessonPlansLanguageFiles.put(file.getParent().getFileName().toString(), file.toFile());
}
if ( file.getFileName().toString().endsWith(".java")) {
javaSource = file.toFile();
}
}
} }
} }
public Plugin(Class<AbstractLesson> lesson, Path pluginDirectory, Map<String, File> lessonPlansLanguageFiles, public Plugin(Path pluginDirectory) {
Map<String, File> solutionLanguageFiles, File lessonSourceFile) {
this.lesson = lesson;
this.pluginDirectory = pluginDirectory; this.pluginDirectory = pluginDirectory;
this.lessonPlansLanguageFiles = lessonPlansLanguageFiles; }
this.solutionLanguageFiles = solutionLanguageFiles;
this.lessonSourceFile = lessonSourceFile; public void loadClasses(Map<String, byte[]> classes) {
for (Map.Entry<String, byte[]> clazz : classes.entrySet()) {
loadClass(clazz.getKey(), clazz.getValue());
}
if (lesson == null) {
throw new PluginLoadingFailure(String
.format("Lesson class not found, following classes were detected in the plugin: %s",
StringUtils.collectionToCommaDelimitedString(classes.keySet())));
}
}
private void loadClass(String name, byte[] classFile) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
PluginClassLoader pluginClassLoader = new PluginClassLoader(contextClassLoader, classFile);
try {
String realClassName = name.replaceFirst("/", "").replaceAll("/", ".").replaceAll(".class", "");
Class clazz = pluginClassLoader.loadClass(realClassName);
if (AbstractLesson.class.isAssignableFrom(clazz)) {
this.lesson = clazz;
}
} catch (ClassNotFoundException e) {
logger.error("Unable to load class {}", name);
}
}
public void loadFiles(List<Path> files) {
for (Path file : files) {
if (fileEndsWith(file, ".html") && hasParentDirectoryWithName(file, NAME_LESSON_SOLUTION_DIRECTORY)) {
solutionLanguageFiles.put(file.getParent().getFileName().toString(), file.toFile());
}
if (fileEndsWith(file, ".html") && hasParentDirectoryWithName(file, NAME_LESSON_PLANS_DIRECTORY)) {
lessonPlansLanguageFiles.put(file.getParent().getFileName().toString(), file.toFile());
}
if (fileEndsWith(file, ".java")) {
lessonSourceFile = file.toFile();
}
if (fileEndsWith(file, ".properties") && hasParentDirectoryWithName(file, NAME_LESSON_I18N_DIRECTORY)) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Files.copy(file, bos);
Path propertiesPath = createPropertiesDirectory();
ResourceBundleClassLoader.setPropertiesPath(propertiesPath);
Files.write(propertiesPath.resolve(file.getFileName()), bos.toByteArray(),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException io) {
throw new PluginLoadingFailure("Property file detected, but unable to copy the properties", io);
}
}
}
}
private Path createPropertiesDirectory() throws IOException {
if (Files.exists(pluginDirectory.resolve(NAME_LESSON_I18N_DIRECTORY))) {
return pluginDirectory.resolve(NAME_LESSON_I18N_DIRECTORY);
} else {
return Files.createDirectory(pluginDirectory.resolve(NAME_LESSON_I18N_DIRECTORY));
}
} }
public Class<AbstractLesson> getLesson() { public Class<AbstractLesson> getLesson() {
@ -107,7 +113,9 @@ public class Plugin {
return this.solutionLanguageFiles; return this.solutionLanguageFiles;
} }
public File getLessonSource() { return lessonSourceFile; } public File getLessonSource() {
return lessonSourceFile;
}
public Map<String, File> getLessonPlans() { public Map<String, File> getLessonPlans() {
return this.lessonPlansLanguageFiles; return this.lessonPlansLanguageFiles;

View File

@ -0,0 +1,22 @@
package org.owasp.webgoat.plugins;
import java.nio.file.Path;
public class PluginFileUtils {
public static boolean fileEndsWith(Path p, String s) {
return p.getFileName().toString().endsWith(s);
}
public static boolean hasParentDirectoryWithName(Path p, String s) {
if (p == null || p.getParent() == null || p.getRoot().equals(p.getParent())) {
return false;
}
if (p.getParent().getFileName().toString().equals(s)) {
return true;
}
return hasParentDirectoryWithName(p.getParent(), s);
}
}

View File

@ -31,11 +31,10 @@ public class PluginsLoader implements Runnable {
try { try {
PluginExtractor extractor = new PluginExtractor(file); PluginExtractor extractor = new PluginExtractor(file);
extractor.extract(); extractor.extract();
Plugin.Builder builder = new Plugin.Builder(); Plugin plugin = new Plugin(extractor.getBaseDirectory());
builder.loadClasses(extractor.getClasses()); plugin.loadClasses(extractor.getClasses());
builder.loadFiles(extractor.getFiles()); plugin.loadFiles(extractor.getFiles());
builder.setBaseDirectory(extractor.getBaseDirectory()); plugins.add(plugin);
plugins.add(builder.build());
} catch (Plugin.PluginLoadingFailure e) { } catch (Plugin.PluginLoadingFailure e) {
logger.error("Unable to load plugin, continue reading others..."); logger.error("Unable to load plugin, continue reading others...");
} }

View File

@ -0,0 +1,33 @@
package org.owasp.webgoat.plugins;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class ResourceBundleClassLoader {
private final static ResourceBundleClassLoader classLoader = new ResourceBundleClassLoader();
private Path propertiesPath;
private ResourceBundleClassLoader() {
}
public static void setPropertiesPath(Path path) {
classLoader.propertiesPath = path;
}
public static ClassLoader createPropertyFilesClassLoader(ClassLoader parentClassLoader) {
final List<URL> urls = new ArrayList<>();
try {
urls.add(classLoader.propertiesPath.toUri().toURL());
} catch (IOException e) {
throw new Plugin.PluginLoadingFailure("Unable to load the properties for the classloader", e);
}
return new URLClassLoader(urls.toArray(new URL[urls.size()]), Thread.currentThread().getContextClassLoader());
}
}

View File

@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -313,7 +314,9 @@ public class Course {
logger.error("Plugins directory {} not found", path); logger.error("Plugins directory {} not found", path);
return; return;
} }
List<Plugin> plugins = new PluginsLoader(Paths.get(path)).loadPlugins(); Path pluginDirectory = Paths.get(path);
webgoatContext.setPluginDirectory(pluginDirectory);
List<Plugin> plugins = new PluginsLoader(pluginDirectory).loadPlugins();
for (Plugin plugin : plugins) { for (Plugin plugin : plugins) {
try { try {
Class<AbstractLesson> c = plugin.getLesson(); Class<AbstractLesson> c = plugin.getLesson();
@ -330,7 +333,7 @@ public class Course {
for(Map.Entry<String, File> lessonPlan : plugin.getLessonPlans().entrySet()) { for(Map.Entry<String, File> lessonPlan : plugin.getLessonPlans().entrySet()) {
lesson.setLessonPlanFileName(lessonPlan.getKey(), lessonPlan.getValue().toString()); lesson.setLessonPlanFileName(lessonPlan.getKey(), lessonPlan.getValue().toString());
} }
lesson.setLessonSolutionFileName(plugin.getLessonPlans().get("en").toString()); lesson.setLessonSolutionFileName(plugin.getLessonSolutions().get("en").toString());
lesson.setSourceFileName(plugin.getLessonSource().toString()); lesson.setSourceFileName(plugin.getLessonSource().toString());
} catch (Exception e) { } catch (Exception e) {
logger.error("Error in loadLessons: ", e); logger.error("Error in loadLessons: ", e);
@ -433,4 +436,5 @@ public class Course {
//loadResources(); //loadResources();
} }
} }

View File

@ -1,9 +1,11 @@
package org.owasp.webgoat.session; package org.owasp.webgoat.session;
import javax.servlet.http.HttpServlet;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServlet;
import java.nio.file.Path;
public class WebgoatContext { public class WebgoatContext {
final Logger logger = LoggerFactory.getLogger(WebgoatContext.class); final Logger logger = LoggerFactory.getLogger(WebgoatContext.class);
@ -80,6 +82,8 @@ public class WebgoatContext {
private String defaultLanguage; private String defaultLanguage;
private java.nio.file.Path pluginDirectory;
public WebgoatContext(HttpServlet servlet) { public WebgoatContext(HttpServlet servlet) {
this.servlet = servlet; this.servlet = servlet;
databaseConnectionString = getParameter(servlet, DATABASE_CONNECTION_STRING); databaseConnectionString = getParameter(servlet, DATABASE_CONNECTION_STRING);
@ -213,4 +217,12 @@ public class WebgoatContext {
return defaultLanguage; return defaultLanguage;
} }
public Path getPluginDirectory() {
return pluginDirectory;
}
public void setPluginDirectory(Path pluginDirectory) {
this.pluginDirectory = pluginDirectory;
}
} }

View File

@ -1,6 +1,7 @@
package org.owasp.webgoat.util; package org.owasp.webgoat.util;
import org.owasp.webgoat.plugins.ResourceBundleClassLoader;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
@ -38,7 +39,7 @@ import java.util.ResourceBundle;
@Component @Component
public class LabelProvider public class LabelProvider
{ {
public final static String DEFAULT_LANGUAGE = "en"; public final static String DEFAULT_LANGUAGE = Locale.ENGLISH.getLanguage();
private final HashMap<Locale, ResourceBundle> labels = new HashMap<Locale, ResourceBundle>(); private final HashMap<Locale, ResourceBundle> labels = new HashMap<Locale, ResourceBundle>();
private final WebGoatResourceBundleController localeController = new WebGoatResourceBundleController(); private final WebGoatResourceBundleController localeController = new WebGoatResourceBundleController();
@ -47,7 +48,8 @@ public class LabelProvider
{ {
if (!labels.containsKey(locale)) if (!labels.containsKey(locale))
{ {
ResourceBundle resBundle = ResourceBundle.getBundle("WebGoatLabels", locale, localeController); ClassLoader classLoader = ResourceBundleClassLoader.createPropertyFilesClassLoader(ResourceBundle.class.getClassLoader());
ResourceBundle resBundle = ResourceBundle.getBundle("WebGoatLabels", new Locale(DEFAULT_LANGUAGE), classLoader);
labels.put(locale, resBundle); labels.put(locale, resBundle);
} }
return labels.get(locale).getString(strName); return labels.get(locale).getString(strName);

View File

@ -1,21 +0,0 @@
#General
LessonCompleted=Congratulations. You have successfully completed this lesson.
RestartLesson=Restart this Lesson
SolutionVideos=Solution Videos
ErrorGenerating=Error generating
InvalidData=Invalid Data
Go!=Go!
#StringSqlInjection.java
StringSqlInjectionSecondStage=Now that you have successfully performed an SQL injection, try the same type of attack on a parameterized query. Restart the lesson if you wish to return to the injectable query.
EnterLastName=Enter your last name:
NoResultsMatched=No results matched. Try Again.
SqlStringInjectionHint1=The application is taking your input and inserting it at the end of a pre-formed SQL command.
SqlStringInjectionHint2=This is the code for the query being built and issued by WebGoat:<br><br> "SELECT * FROM user_data WHERE last_name = "accountName"
SqlStringInjectionHint3=Compound SQL statements can be made by joining multiple tests with keywords like AND and OR. Try appending a SQL statement that always resolves to true
SqlStringInjectionHint4=Try entering [ smith' OR '1' = '1 ].

View File

@ -1,8 +0,0 @@
#General
LessonCompleted=Herzlichen Gl\u00fcckwunsch! Sie haben diese Lektion erfolgreich abgeschlossen.
RestartLesson=Lektion neu beginnen
SolutionVideos=L\u00f6sungsvideos
ErrorGenerating=Fehler beim Generieren von
InvalidData=Ung\u00fcltige Daten

View File

@ -1,7 +0,0 @@
#General
LessonCompleted=F\u00e9licitations. Vous avez termin\u00e9 cette le\u00e7on avec succ\u00e9s.
RestartLesson=Recommencer cette le\u00e7on
SolutionVideos=Solution vid\u00e9os
ErrorGenerating=Error generating
InvalidData=Donn\u00e9e invalide

View File

@ -1,7 +0,0 @@
#General
LessonCompleted=\u041f\u043e\u0437\u0434\u0440\u0430\u0432\u043b\u044f\u044e. \u0412\u044b \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043f\u0440\u043e\u0448\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0439 \u0443\u0440\u043e\u043a.
RestartLesson=\u041d\u0430\u0447\u0430\u043b\u044c \u0441\u043d\u0430\u0447\u0430\u043b\u0430
SolutionVideos=\u0412\u0438\u0434\u0435\u043e \u0441 \u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c
ErrorGenerating=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430
InvalidData=\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
ace.define("ace/snippets/jsp",["require","exports","module"],function(e,t,n){"use strict";t.snippetText='snippet @page\n <%@page contentType="text/html" pageEncoding="UTF-8"%>\nsnippet jstl\n <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>\n <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>\nsnippet jstl:c\n <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>\nsnippet jstl:fn\n <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>\nsnippet cpath\n ${pageContext.request.contextPath}\nsnippet cout\n <c:out value="${1}" default="${2}" />\nsnippet cset\n <c:set var="${1}" value="${2}" />\nsnippet cremove\n <c:remove var="${1}" scope="${2:page}" />\nsnippet ccatch\n <c:catch var="${1}" />\nsnippet cif\n <c:if test="${${1}}">\n ${2}\n </c:if>\nsnippet cchoose\n <c:choose>\n ${1}\n </c:choose>\nsnippet cwhen\n <c:when test="${${1}}">\n ${2}\n </c:when>\nsnippet cother\n <c:otherwise>\n ${1}\n </c:otherwise>\nsnippet cfore\n <c:forEach items="${${1}}" var="${2}" varStatus="${3}">\n ${4:<c:out value="$2" />}\n </c:forEach>\nsnippet cfort\n <c:set var="${1}">${2:item1,item2,item3}</c:set>\n <c:forTokens var="${3}" items="${$1}" delims="${4:,}">\n ${5:<c:out value="$3" />}\n </c:forTokens>\nsnippet cparam\n <c:param name="${1}" value="${2}" />\nsnippet cparam+\n <c:param name="${1}" value="${2}" />\n cparam+${3}\nsnippet cimport\n <c:import url="${1}" />\nsnippet cimport+\n <c:import url="${1}">\n <c:param name="${2}" value="${3}" />\n cparam+${4}\n </c:import>\nsnippet curl\n <c:url value="${1}" var="${2}" />\n <a href="${$2}">${3}</a>\nsnippet curl+\n <c:url value="${1}" var="${2}">\n <c:param name="${4}" value="${5}" />\n cparam+${6}\n </c:url>\n <a href="${$2}">${3}</a>\nsnippet credirect\n <c:redirect url="${1}" />\nsnippet contains\n ${fn:contains(${1:string}, ${2:substr})}\nsnippet contains:i\n ${fn:containsIgnoreCase(${1:string}, ${2:substr})}\nsnippet endswith\n ${fn:endsWith(${1:string}, ${2:suffix})}\nsnippet escape\n ${fn:escapeXml(${1:string})}\nsnippet indexof\n ${fn:indexOf(${1:string}, ${2:substr})}\nsnippet join\n ${fn:join(${1:collection}, ${2:delims})}\nsnippet length\n ${fn:length(${1:collection_or_string})}\nsnippet replace\n ${fn:replace(${1:string}, ${2:substr}, ${3:replace})}\nsnippet split\n ${fn:split(${1:string}, ${2:delims})}\nsnippet startswith\n ${fn:startsWith(${1:string}, ${2:prefix})}\nsnippet substr\n ${fn:substring(${1:string}, ${2:begin}, ${3:end})}\nsnippet substr:a\n ${fn:substringAfter(${1:string}, ${2:substr})}\nsnippet substr:b\n ${fn:substringBefore(${1:string}, ${2:substr})}\nsnippet lc\n ${fn:toLowerCase(${1:string})}\nsnippet uc\n ${fn:toUpperCase(${1:string})}\nsnippet trim\n ${fn:trim(${1:string})}\n',t.scope="jsp"}) ace.define("ace/snippets/jsp",["require","exports","module"],function(e,t,n){"use strict";t.snippetText='snippet @page\n <%@page contentType="text/html" pageEncoding="UTF-8"%>\nsnippet jstl\n <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>\n <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>\nsnippet jstl:c\n <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>\nsnippet jstl:fn\n <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>\nsnippet cpath\n ${pageContext.request.contextPath}\nsnippet cout\n <c:out value="${1}" default="${2}" />\nsnippet cset\n <c:set var="${1}" value="${2}" />\nsnippet cremove\n <c:remove var="${1}" scope="${2:page}" />\nsnippet ccatch\n <c:catch var="${1}" />\nsnippet cif\n <c:if test="${${1}}">\n ${2}\n </c:if>\nsnippet cchoose\n <c:choose>\n ${1}\n </c:choose>\nsnippet cwhen\n <c:when test="${${1}}">\n ${2}\n </c:when>\nsnippet cother\n <c:otherwise>\n ${1}\n </c:otherwise>\nsnippet cfore\n <c:forEach items="${${1}}" var="${2}" varStatus="${3}">\n ${4:<c:out value="$2" />}\n </c:forEach>\nsnippet cfort\n <c:set var="${1}">${2:item1,item2,item3}</c:set>\n <c:forTokens var="${3}" items="${$1}" delims="${4:,}">\n ${5:<c:out value="$3" />}\n </c:forTokens>\nsnippet cparam\n <c:param name="${1}" value="${2}" />\nsnippet cparam+\n <c:param name="${1}" value="${2}" />\n cparam+${3}\nsnippet cimport\n <c:import url="${1}" />\nsnippet cimport+\n <c:import url="${1}">\n <c:param name="${2}" value="${3}" />\n cparam+${4}\n </c:import>\nsnippet curl\n <c:url value="${1}" var="${2}" />\n <a href="${$2}">${3}</a>\nsnippet curl+\n <c:url value="${1}" var="${2}">\n <c:param name="${4}" value="${5}" />\n cparam+${6}\n </c:url>\n <a href="${$2}">${3}</a>\nsnippet credirect\n <c:redirect url="${1}" />\nsnippet contains\n ${fn:contains(${1:string}, ${2:substr})}\nsnippet contains:i\n ${fn:containsIgnoreCase(${1:string}, ${2:substr})}\nsnippet endswith\n ${fn:fileEndsWith(${1:string}, ${2:suffix})}\nsnippet escape\n ${fn:escapeXml(${1:string})}\nsnippet indexof\n ${fn:indexOf(${1:string}, ${2:substr})}\nsnippet join\n ${fn:join(${1:collection}, ${2:delims})}\nsnippet length\n ${fn:length(${1:collection_or_string})}\nsnippet replace\n ${fn:replace(${1:string}, ${2:substr}, ${3:replace})}\nsnippet split\n ${fn:split(${1:string}, ${2:delims})}\nsnippet startswith\n ${fn:startsWith(${1:string}, ${2:prefix})}\nsnippet substr\n ${fn:substring(${1:string}, ${2:begin}, ${3:end})}\nsnippet substr:a\n ${fn:substringAfter(${1:string}, ${2:substr})}\nsnippet substr:b\n ${fn:substringBefore(${1:string}, ${2:substr})}\nsnippet lc\n ${fn:toLowerCase(${1:string})}\nsnippet uc\n ${fn:toUpperCase(${1:string})}\nsnippet trim\n ${fn:trim(${1:string})}\n',t.scope="jsp"})