![JAR search and dependency download from the Maven repository](/logo.png)
org.sakaiproject.entitybroker.util.TemplateParseUtil Maven / Gradle / Ivy
The newest version!
/**
* $Id$
* $URL$
* TemplateParseUtil.java - entity-broker - Apr 10, 2008 9:57:29 AM - azeckoski
**************************************************************************
* Copyright (c) 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sakaiproject.entitybroker.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.sakaiproject.entitybroker.EntityView;
import org.sakaiproject.entitybroker.entityprovider.extension.Formats;
/**
* Utility class to handle the URL template parsing (entity template parsing)
*
* @author Aaron Zeckoski ([email protected])
*/
public class TemplateParseUtil {
public static final char SEPARATOR = EntityView.SEPARATOR;
public static final char PERIOD = EntityView.PERIOD;
public static final String BRACES = "[\\{\\}]";
/**
* The entity prefix marker (Example value: "myprefix")
*/
public static final String PREFIX = EntityView.PREFIX;
/**
* The entity ID marker (Example value: "123")
*/
public static final String ID = EntityView.ID;
/**
* The entity extension (format) marker (Example value: "xml")
*/
public static final String EXTENSION = "extension";
/**
* The extension with a period in front marker (Example value: ".xml")
*/
public static final String DOT_EXTENSION = "dot-extension";
/**
* The value in the query string (without a leading ?), '' if non available (Example value: "auto=true")
*/
public static final String QUERY_STRING = "query-string";
/**
* The value in the query string (with a leading ?), '' if non available (Example value: "?auto=true")
*/
public static final String QUESTION_QUERY_STRING = "question-query-string";
public static final String PREFIX_VARIABLE = "{"+PREFIX+"}";
public static final String TEMPLATE_PREFIX = SEPARATOR + PREFIX_VARIABLE;
public static final String DIRECT_PREFIX = EntityView.DIRECT_PREFIX;
public static final String DIRECT_PREFIX_SLASH = DIRECT_PREFIX + SEPARATOR;
/**
* Defines the valid chars for a replacement variable
*/
public static final String VALID_VAR_CHARS = "[\\p{L}}\\p{N}\\\\(\\\\)\\+\\*\\.\\-_=,:;!~@% ]";
/**
* Defines the valid chars for a parser input (e.g. entity reference)
*/
public static final String VALID_INPUT_CHARS = "[\\p{L}\\p{N}\\\\\\(\\\\)\\+\\*\\.\\-_=,:;!~@% "+SEPARATOR+"]";
/**
* Defines the valid chars for a template
*/
public static final String VALID_TEMPLATE_CHARS = "[\\p{L}\\p{N}\\\\\\(\\\\)\\+\\*\\.\\-_=,:;&!~@%"+SEPARATOR+"\\{\\}]";
/**
* Defines the valid template chars for an outgoing template (allows ?)
*/
public static final String VALID_TEMPLATE_CHARS_OUTGOING = "[\\p{L}\\p{N}\\\\\\(\\\\)\\+\\*\\.\\-_=,:;&!~@%"+SEPARATOR+"\\{\\}\\?]";
/**
* Defines the parse template for the "list" operation,
* return a list of all records,
* typically /{prefix}
*/
public static final String TEMPLATE_LIST = EntityView.VIEW_LIST;
/**
* Defines the parse template for the "show" operation,
* access a record OR POST operations related to a record,
* typically /{prefix}/{id}
*/
public static final String TEMPLATE_SHOW = EntityView.VIEW_SHOW;
/**
* Defines the parse template for the "new" operation,
* return a form for creating a new record,
* typically /{prefix}/new
*/
public static final String TEMPLATE_NEW = EntityView.VIEW_NEW;
/**
* Defines the parse template for the "edit" operation,
* access the data to modify a record,
* typically /{prefix}/{id}/edit
*/
public static final String TEMPLATE_EDIT = EntityView.VIEW_EDIT;
/**
* Defines the parse template for the "delete" operation,
* access the data to remove a record,
* typically /{prefix}/{id}/delete
*/
public static final String TEMPLATE_DELETE = EntityView.VIEW_DELETE;
/**
* Defines the order that parse templates will be processed in and
* the set of parse template types (keys) which must be defined,
* the first one to match will be used when parsing in a path
*/
public static final String[] PARSE_TEMPLATE_KEYS = {
TEMPLATE_EDIT,
TEMPLATE_DELETE,
TEMPLATE_NEW,
TEMPLATE_SHOW,
TEMPLATE_LIST
};
/**
* Stores the preloaded default templates
*/
public static final List defaultTemplates;
/**
* Stores the preloaded processed default templates
*/
public static final List defaultPreprocessedTemplates;
/**
* Contains a set of all the common extensions
*/
public static Set commonExtensions = new HashSet(20);
// static initializer
static {
defaultTemplates = new ArrayList();
// this load order should match the array above
defaultTemplates.add( new Template(TEMPLATE_EDIT, TEMPLATE_PREFIX + SEPARATOR + "{"+ID+"}" + SEPARATOR + "edit") );
defaultTemplates.add( new Template(TEMPLATE_DELETE, TEMPLATE_PREFIX + SEPARATOR + "{"+ID+"}" + SEPARATOR + "delete") );
defaultTemplates.add( new Template(TEMPLATE_NEW, TEMPLATE_PREFIX + SEPARATOR + "new") );
defaultTemplates.add( new Template(TEMPLATE_SHOW, TEMPLATE_PREFIX + SEPARATOR + "{"+ID+"}") );
defaultTemplates.add( new Template(TEMPLATE_LIST, TEMPLATE_PREFIX) );
defaultPreprocessedTemplates = preprocessTemplates(defaultTemplates);
// populate the list of common extensions
Collections.addAll(commonExtensions, Formats.JSON_EXTENSIONS);
Collections.addAll(commonExtensions, Formats.XML_EXTENSIONS);
Collections.addAll(commonExtensions, Formats.HTML_EXTENSIONS);
Collections.addAll(commonExtensions, Formats.FORM_EXTENSIONS);
Collections.addAll(commonExtensions, Formats.JSONP_EXTENSIONS);
Collections.addAll(commonExtensions, Formats.ATOM_EXTENSIONS);
Collections.addAll(commonExtensions, Formats.RSS_EXTENSIONS);
// also image extensions and other common ones
commonExtensions.add("png");
commonExtensions.add("jpg");
commonExtensions.add("gif");
commonExtensions.add("jpeg");
commonExtensions.add("csv");
}
/**
* Check if a templateKey is valid, if not then throws {@link IllegalArgumentException}
* @param templateKey a key from the set of template keys {@link #PARSE_TEMPLATE_KEYS}
*/
public static void validateTemplateKey(String templateKey) {
boolean found = false;
for (int i = 0; i < PARSE_TEMPLATE_KEYS.length; i++) {
if (PARSE_TEMPLATE_KEYS[i].equals(templateKey)) {
found = true;
break;
}
}
if (! found) {
throw new IllegalArgumentException("Invalid parse template key: " + templateKey);
}
}
/**
* Get a default template for a specific template key
* @param templateKey a key from the set of template keys {@link #PARSE_TEMPLATE_KEYS}
* @return the template
* @throws IllegalArgumentException if the template key is invalid
*/
public static String getDefaultTemplate(String templateKey) {
String template = null;
for (Template t : defaultTemplates) {
if (t.templateKey.equals(templateKey)) {
template = t.template;
}
}
if (template == null) {
throw new IllegalArgumentException("No default template available for this key: " + templateKey);
}
return template;
}
/**
* Validate a template, if invalid then an exception is thrown
* @param template a parse template
*/
public static void validateTemplate(String template) {
if (template == null || "".equals(template)) {
throw new IllegalArgumentException("Template cannot be null or empty string");
} else if (template.charAt(0) != SEPARATOR) {
throw new IllegalArgumentException("Template ("+template+") must start with " + SEPARATOR);
} else if (template.charAt(template.length()-1) == SEPARATOR) {
throw new IllegalArgumentException("Template ("+template+") cannot end with " + SEPARATOR);
} else if (! template.startsWith(TEMPLATE_PREFIX)) {
throw new IllegalArgumentException("Template ("+template+") must start with: " + TEMPLATE_PREFIX
+ " :: that is SEPARATOR + \"{\"+PREFIX+\"}\"");
} else if (template.indexOf("}{") != -1) {
throw new IllegalArgumentException("Template ("+template+") replacement variables ({var}) " +
"cannot be next to each other, " +
"there must be something between each template variable");
} else if (template.indexOf("{}") != -1) {
throw new IllegalArgumentException("Template ("+template+") replacement variables ({var}) " +
"cannot be empty ({}), there must be a value between them");
} else if (! template.matches(VALID_TEMPLATE_CHARS+"+")) {
// take out {} and check if the template uses valid chars
throw new IllegalArgumentException("Template ("+template+") can only contain the following (not counting []): " + VALID_TEMPLATE_CHARS);
}
}
/**
* Validates an outgoing template to make sure it is valid
* @param template an outgoing template,
* if starts with / then it will be used as is and redirected to,
* otherwise it will have the direct URL prefixed and will be forwarded
* @return the template which should be completely valid
*/
public static String validateOutgoingTemplate(String template) {
String validTemplate = template;
if (template == null || "".equals(template)) {
throw new IllegalArgumentException("Template cannot be null or empty string");
} else if (template.indexOf("{}") != -1) {
throw new IllegalArgumentException("Template ("+template+") replacement variables ({var}) " +
"cannot be empty ({}), there must be a value between them");
} else if (! template.matches(VALID_TEMPLATE_CHARS_OUTGOING+"+")) {
// take out {} and check if the template uses valid chars
throw new IllegalArgumentException("Template ("+template+") can only contain the following (not counting []): " + VALID_TEMPLATE_CHARS_OUTGOING);
}
if (template.startsWith(PREFIX_VARIABLE)) {
validTemplate = DIRECT_PREFIX + SEPARATOR + template;
} else if (template.startsWith(TEMPLATE_PREFIX)) {
validTemplate = DIRECT_PREFIX + template;
} else if (SEPARATOR != template.charAt(0)) {
// assume the user wants /direct/ added to the URL
validTemplate = DIRECT_PREFIX + SEPARATOR + template;
}
return validTemplate;
}
/**
* Takes a template and replaces the segment keys with the segment values,
* keys should not have {} around them yet as these will be added around each key
* in the segments map
*
* @param template a parse template with {variables} in it
* @param segments a map of all possible segment keys and values,
* unused keys will be ignored
* @return the template with replacement values filled in
* @throws IllegalArgumentException if all template variables cannot be replaced or template is empty/null
*/
public static String mergeTemplate(String template, Map segments) {
if (template == null || "".equals(template) || segments == null) {
throw new IllegalArgumentException("Cannot operate on null template/segments, template must not be empty");
}
int vars = 0;
char[] chars = template.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] == '{') {
vars++;
}
}
String reference = template;
int replacements = 0;
for (Entry es : segments.entrySet()) {
String keyBraces = "{"+es.getKey()+"}";
if (reference.contains(keyBraces)) {
reference = reference.replace(keyBraces, es.getValue());
replacements++;
}
}
if (replacements != vars) {
throw new IllegalArgumentException("Failed merge, could not replace all variables ("+vars
+") in the template, only replaced " + replacements);
}
return reference;
}
/**
* Find the extension from a string and return the string without the extension and the extension,
* an extension is a period (".") followed by any number of non-periods,
* the original input is returned as the 0th item in the array
* returned array contains 3 strings:
* 0 = the original input string
* 1 = the string without the extension or the original if it has none
* 2 = the extension OR null if there is no extension
*
* @param input any string
* @return an array with the string without the extension or the original if it has none in position 1
* and the extension in the position 2 (or null if no extension), position 0 holds the original input string
*/
public static String[] findExtension(String input) {
// regex pattern: ".*(\\.[^.]+|$)"
String stripped = input;
String extension = null;
if (input != null) {
int extensionLoc = input.lastIndexOf(PERIOD, input.length());
if (extensionLoc == 0) {
// starts with a period so no extension, do nothing
} else {
int sepLoc = input.lastIndexOf(SEPARATOR, input.length());
if (extensionLoc > 0
&& sepLoc < extensionLoc) {
stripped = input.substring(0, extensionLoc);
if ( (input.length() - 1) > extensionLoc) {
extension = input.substring(extensionLoc + 1);
// we only consider it an extension if we recognize the type
if (!commonExtensions.contains(extension)) {
stripped = input;
extension = null;
}
}
}
}
}
return new String[] {input, stripped, extension};
}
/**
* Parse a string and attempt to match it to a template and then
* return the match information along with all the parsed out keys and values
*
* @param input a string which we want to attempt to match to one of the templates
* @param preprocessed the analyzed templates to attempt to match in the order they should attempt the match,
* can be a single template or multiples, use {@link #preprocessTemplates(List)} to create this
* (recommend caching the preprocessed templates to avoid reprocessing them over and over)
* @return a the processed template analysis object OR null if no matches
*/
public static ProcessedTemplate parseTemplate(String input, List preprocessed) {
if (preprocessed == null) {
preprocessed = defaultPreprocessedTemplates;
}
if (input == null || "".equals(input)) {
throw new IllegalArgumentException("input cannot be null or empty");
}
if (! input.matches(VALID_INPUT_CHARS+"+")) {
throw new IllegalArgumentException("input must consist of the following chars only (not counting []): " + VALID_INPUT_CHARS);
}
ProcessedTemplate analysis = null;
Map segments = new HashMap();
// strip off the extension if there is one
String[] ext = findExtension(input);
input = ext[1];
String extension = ext[2];
// try to get matches
for (PreProcessedTemplate ppt : preprocessed) {
segments.clear();
String regex = ppt.regex + "(?:/"+VALID_INPUT_CHARS+"+|$)"; // match extras if there are any (greedy match)
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
if ( m.matches() ) {
if ( m.groupCount() == ppt.variableNames.size() ) {
for (int j = 0; j < m.groupCount(); j++) {
String subseq = m.group(j+1); // ignore first group, it is the whole pattern
if (subseq != null) {
segments.put(ppt.variableNames.get(j), subseq);
}
}
// fill in the analysis object
analysis = new ProcessedTemplate(ppt.templateKey, ppt.template, regex,
new ArrayList(ppt.variableNames),
new HashMap(segments), extension);
break;
}
}
}
if (analysis == null) {
// no matches so should we die?
}
return analysis;
}
/**
* Process the templates before attempting to match them,
* this is here so we can reduce the load of reprocessing the same templates over and over
* @param templates the templates to attempt to preprocess, can be a single template or multiples
* @return the list of preprocessed templates (in the same order as input)
*/
public static List preprocessTemplates(List templates) {
if (templates == null) {
templates = defaultTemplates;
}
List analyzedTemplates = new ArrayList();
for (Template t : templates) {
analyzedTemplates.add( preprocessTemplate(t) );
}
return analyzedTemplates;
}
/**
* process a template into a preprocessed template which can be cached
* @param t the template
* @return the preprocessed template
*/
public static PreProcessedTemplate preprocessTemplate(Template t) {
if (t.incoming) {
TemplateParseUtil.validateTemplate(t.template);
} else {
t.template = TemplateParseUtil.validateOutgoingTemplate(t.template);
}
List vars = new ArrayList();
StringBuilder regex = new StringBuilder();
String[] parts = t.template.split(BRACES);
for (int j = 0; j < parts.length; j++) {
String part = parts[j];
if (j % 2 == 0) {
// odd parts are textual breaks
// check for regex chars and escape them "[A-Za-z0-9\\\\(\\\\)\\.\\-_.=,:;&!~"+SEPARATOR+"\\{\\}\\?]"
regex.append(part.replace("?", "\\?").replace(".", "\\.").replace("*", "\\*").replace("+", "\\+")
.replace("-", "\\-").replace("(", "\\\\(").replace(")", "\\\\)"));
} else {
// even parts are replacement vars
vars.add(part);
regex.append("(");
regex.append(VALID_VAR_CHARS);
regex.append("+)");
}
}
return new PreProcessedTemplate(t.templateKey,
t.template, regex.toString(), new ArrayList(vars));
}
/**
* Represents a parseable template (which is basically a key and the template string),
* the array which defines the set of template keys is {@link #PARSE_TEMPLATE_KEYS}
* Rules for parse templates:
* 1) "{","}", and {@link #SEPARATOR} are special characters and must be used as indicated only
* 2) Must begin with a {@link #SEPARATOR}, must not end with a {@link #SEPARATOR}
* 3) must begin with "/{prefix}" (use the {@link #SEPARATOR} and {@link #PREFIX} constants)
* 3) each {var} can only be used once in a template
* 4) {var} can never touch each other (i.e /{var1}{var2}/{id} is invalid)
* 5) each {var} can only have the chars from {@link TemplateParseUtil#VALID_VAR_CHARS}
* 6) parse templates can only have the chars from {@link TemplateParseUtil#VALID_TEMPLATE_CHARS}
* 7) Empty braces ({}) cannot appear in the template
*
* @author Aaron Zeckoski ([email protected])
*/
public static class Template {
/**
* the template key, from the set of template keys {@link #PARSE_TEMPLATE_KEYS},
* or make one up for your own templates, should be unique for this set of templates
*/
public String templateKey;
/**
* the template itself
*/
public String template;
/**
* indicates the template is an incoming template if true, outgoing template if false
*/
public boolean incoming = true;
/**
* Used to create a template for loading, defaults to an incoming template
* @param templateKey template identifier, from the set of template keys {@link #PARSE_TEMPLATE_KEYS},
* must be unique for this set of templates
* @param template the parseable template
*/
public Template(String templateKey, String template) {
if (templateKey == null || "".equals(templateKey)) {
templateKey = UUID.randomUUID().toString();
}
this.templateKey = templateKey;
this.template = template;
}
/**
* Used to create a template for loading
* @param templateKey template identifier, from the set of template keys {@link #PARSE_TEMPLATE_KEYS},
* must be unique for this set of templates
* @param template the parseable template
* @param incoming if true then this is an incoming template, otherwise it is an outgoing one
*/
public Template(String templateKey, String template, boolean incoming) {
this(templateKey, template);
this.incoming = incoming;
}
@Override
public boolean equals(Object obj) {
if (null == obj) return false;
if (!(obj instanceof Template)) return false;
else {
Template castObj = (Template) obj;
if (null == this.templateKey || null == castObj.templateKey) return false;
else return (
this.templateKey.equals(castObj.templateKey)
);
}
}
@Override
public int hashCode() {
if (null == this.templateKey) return super.hashCode();
String hashStr = this.getClass().getName() + ":" + this.templateKey.hashCode();
return hashStr.hashCode();
}
}
/**
* Contains the data for templates,
* each template must have a template key and the template itself
*
* @author Aaron Zeckoski ([email protected])
*/
public static class PreProcessedTemplate extends Template {
/**
* The regular expression to match this template exactly
*/
public String regex;
/**
* The list of variable names found in this template
*/
public List variableNames;
protected PreProcessedTemplate(String templateKey, String template, String regex, List variableNames) {
super(templateKey, template);
this.regex = regex;
this.variableNames = variableNames;
}
}
/**
* Contains the processed template with the values from the processed input string
* that was determined to be related to this template
*
* @author Aaron Zeckoski ([email protected])
*/
public static class ProcessedTemplate extends PreProcessedTemplate {
/**
* The list of segment values (variableName -> matched value),
* this will be filled in by the {@link TemplateParseUtil#parseTemplate(String, Map)} method
* and will be null otherwise
*/
public Map segmentValues;
/**
* The extension found while processing the input string,
* null if none could be found
*/
public String extension;
public ProcessedTemplate(String templateKey, String template, String regex,
List variableNames, Map segmentValues, String extension) {
super(templateKey, template, regex, variableNames);
this.segmentValues = segmentValues;
this.extension = extension;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy