diff --git a/guacamole-ext/pom.xml b/guacamole-ext/pom.xml index e175b300d..ba0b84813 100644 --- a/guacamole-ext/pom.xml +++ b/guacamole-ext/pom.xml @@ -112,6 +112,14 @@ compile + + + junit + junit + 4.10 + test + + diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java new file mode 100644 index 000000000..527942bfc --- /dev/null +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.token; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Filtering object which replaces tokens of the form "${TOKEN_NAME}" with + * their corresponding values. Unknown tokens are not replaced. If TOKEN_NAME + * is a valid token, the literal value "${TOKEN_NAME}" can be included by using + * "$${TOKEN_NAME}". + * + * @author Michael Jumper + */ +public class TokenFilter { + + /** + * Regular expression which matches individual tokens, with additional + * capturing groups for convenient retrieval of leading text, the possible + * escape character preceding the token, the name of the token, and the + * entire token itself. + */ + private final Pattern tokenPattern = Pattern.compile("(.*?)(^|.)(\\$\\{([A-Za-z0-9_]*)\\})"); + + /** + * The index of the capturing group within tokenPattern which matches + * non-token text preceding a possible token. + */ + private static final int LEADING_TEXT_GROUP = 1; + + /** + * The index of the capturing group within tokenPattern which matches the + * character immediately preceding a possible token, possibly denoting that + * the token should instead be interpreted as a literal. + */ + private static final int ESCAPE_CHAR_GROUP = 2; + + /** + * The index of the capturing group within tokenPattern which matches the + * entire token, including the leading "${" and terminating "}" strings. + */ + private static final int TOKEN_GROUP = 3; + + /** + * The index of the capturing group within tokenPattern which matches only + * the token name contained within the "${" and "}" strings. + */ + private static final int TOKEN_NAME_GROUP = 4; + + /** + * The values of all known tokens. + */ + private final Map tokenValues = new HashMap(); + + /** + * Sets the token having the given name to the given value. Any existing + * value for that token is replaced. + * + * @param name + * The name of the token to set. + * + * @param value + * The value to set the token to. + */ + public void setToken(String name, String value) { + tokenValues.put(name, value); + } + + /** + * Returns the value of the token with the given name, or null if no such + * token has been set. + * + * @param name + * The name of the token to return. + * + * @return + * The value of the token with the given name, or null if no such + * token exists. + */ + public String getToken(String name) { + return tokenValues.get(name); + } + + /** + * Removes the value of the token with the given name. If no such token + * exists, this function has no effect. + * + * @param name + * The name of the token whose value should be removed. + */ + public void unsetToken(String name) { + tokenValues.remove(name); + } + + /** + * Returns a map of all tokens, with each key being a token name, and each + * value being the corresponding token value. Changes to this map will + * directly affect the tokens associated with this filter. + * + * @return + * A map of all token names and their corresponding values. + */ + public Map getTokens() { + return tokenValues; + } + + /** + * Replaces all current token values with the contents of the given map, + * where each map key represents a token name, and each map value + * represents a token value. + * + * @param tokens + * A map containing the token names and corresponding values to + * assign. + */ + public void setTokens(Map tokens) { + tokenValues.clear(); + tokenValues.putAll(tokens); + } + + /** + * Filters the given string, replacing any tokens with their corresponding + * values. + * + * @param input + * The string to filter. + * + * @return + * A copy of the input string, with any tokens replaced with their + * corresponding values. + */ + public String filter(String input) { + + StringBuilder output = new StringBuilder(); + Matcher tokenMatcher = tokenPattern.matcher(input); + + // Track last regex match + int endOfLastMatch = 0; + + // For each possible token + while (tokenMatcher.find()) { + + // Pull possible leading text and first char before possible token + String literal = tokenMatcher.group(LEADING_TEXT_GROUP); + String escape = tokenMatcher.group(ESCAPE_CHAR_GROUP); + + // Append leading non-token text + output.append(literal); + + // If char before token is '$', the token itself is escaped + if ("$".equals(escape)) { + String notToken = tokenMatcher.group(TOKEN_GROUP); + output.append(notToken); + } + + // If char is not '$', interpret as a token + else { + + // The char before the token, if any, is a literal + output.append(escape); + + // Pull token value + String tokenName = tokenMatcher.group(TOKEN_NAME_GROUP); + String tokenValue = getToken(tokenName); + + // If token is unknown, interpret as literal + if (tokenValue == null) { + String notToken = tokenMatcher.group(TOKEN_GROUP); + output.append(notToken); + } + + // Otherwise, substitute value + else + output.append(tokenValue); + + } + + // Update last regex match + endOfLastMatch = tokenMatcher.end(); + + } + + // Append any remaining non-token text + output.append(input.substring(endOfLastMatch)); + + return output.toString(); + + } + +} diff --git a/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.java b/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.java new file mode 100644 index 000000000..d1c433b00 --- /dev/null +++ b/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.token; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Test which verifies the filtering functionality of TokenFilter. + * + * @author Michael Jumper + */ +public class TokenFilterTest { + + /** + * Verifies that token replacement via filter() functions as specified. + */ + @Test + public void testFilter() { + + // Create token filter + TokenFilter tokenFilter = new TokenFilter(); + tokenFilter.setToken("TOKEN_A", "value-of-a"); + tokenFilter.setToken("TOKEN_B", "value-of-b"); + + // Test basic substitution and escaping + assertEquals( + "$${NOPE}hellovalue-of-aworldvalue-of-b${NOT_A_TOKEN}", + tokenFilter.filter("$$${NOPE}hello${TOKEN_A}world${TOKEN_B}$${NOT_A_TOKEN}") + ); + + // Unknown tokens must be interpreted as literals + assertEquals( + "${NOPE}hellovalue-of-aworld${TOKEN_C}", + tokenFilter.filter("${NOPE}hello${TOKEN_A}world${TOKEN_C}") + ); + + } + +}