| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache 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.apache.org/licenses/LICENSE-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 freemarker.ext.servlet; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.regex.Pattern; |
| |
| import javax.servlet.ServletContext; |
| |
| import freemarker.cache.ClassTemplateLoader; |
| import freemarker.cache.FileTemplateLoader; |
| import freemarker.cache.MultiTemplateLoader; |
| import freemarker.cache.TemplateLoader; |
| import freemarker.cache.WebappTemplateLoader; |
| import freemarker.core._ObjectBuilderSettingEvaluator; |
| import freemarker.core._SettingEvaluationEnvironment; |
| import freemarker.log.Logger; |
| import freemarker.template.Configuration; |
| import freemarker.template._TemplateAPI; |
| import freemarker.template.utility.StringUtil; |
| |
| |
| final class InitParamParser { |
| |
| static final String TEMPLATE_PATH_PREFIX_CLASS = "class://"; |
| static final String TEMPLATE_PATH_PREFIX_CLASSPATH = "classpath:"; |
| static final String TEMPLATE_PATH_PREFIX_FILE = "file://"; |
| static final String TEMPLATE_PATH_SETTINGS_BI_NAME = "settings"; |
| |
| private static final Logger LOG = Logger.getLogger("freemarker.servlet"); |
| |
| private InitParamParser() { |
| // Not to be instantiated |
| } |
| |
| static TemplateLoader createTemplateLoader( |
| String templatePath, Configuration cfg, Class classLoaderClass, ServletContext srvCtx) |
| throws IOException { |
| final int settingAssignmentsStart = findTemplatePathSettingAssignmentsStart(templatePath); |
| String pureTemplatePath = (settingAssignmentsStart == -1 ? templatePath : templatePath.substring(0, settingAssignmentsStart)) |
| .trim(); |
| |
| final TemplateLoader templateLoader; |
| if (pureTemplatePath.startsWith(TEMPLATE_PATH_PREFIX_CLASS)) { |
| String packagePath = pureTemplatePath.substring(TEMPLATE_PATH_PREFIX_CLASS.length()); |
| packagePath = normalizeToAbsolutePackagePath(packagePath); |
| templateLoader = new ClassTemplateLoader(classLoaderClass, packagePath); |
| } else if (pureTemplatePath.startsWith(TEMPLATE_PATH_PREFIX_CLASSPATH)) { |
| // To be similar to Spring resource paths, we don't require "//": |
| String packagePath = pureTemplatePath.substring(TEMPLATE_PATH_PREFIX_CLASSPATH.length()); |
| packagePath = normalizeToAbsolutePackagePath(packagePath); |
| |
| ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); |
| if (classLoader == null) { |
| LOG.warn("No Thread Context Class Loader was found. Falling back to the class loader of " |
| + classLoaderClass.getName() + "."); |
| classLoader = classLoaderClass.getClassLoader(); |
| } |
| |
| templateLoader = new ClassTemplateLoader(classLoader, packagePath); |
| } else if (pureTemplatePath.startsWith(TEMPLATE_PATH_PREFIX_FILE)) { |
| String filePath = pureTemplatePath.substring(TEMPLATE_PATH_PREFIX_FILE.length()); |
| templateLoader = new FileTemplateLoader(new File(filePath)); |
| } else if (pureTemplatePath.startsWith("[") |
| && cfg.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_22) { |
| if (!pureTemplatePath.endsWith("]")) { |
| // B.C. constraint: Can't throw any checked exceptions. |
| throw new TemplatePathParsingException("Failed to parse template path; closing \"]\" is missing."); |
| } |
| String commaSepItems = pureTemplatePath.substring(1, pureTemplatePath.length() - 1).trim(); |
| List listItems = parseCommaSeparatedTemplatePaths(commaSepItems); |
| TemplateLoader[] templateLoaders = new TemplateLoader[listItems.size()]; |
| for (int i = 0; i < listItems.size(); i++) { |
| String pathItem = (String) listItems.get(i); |
| templateLoaders[i] = createTemplateLoader(pathItem, cfg, classLoaderClass, srvCtx); |
| } |
| templateLoader = new MultiTemplateLoader(templateLoaders); |
| } else if (pureTemplatePath.startsWith("{") |
| && cfg.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_22) { |
| throw new TemplatePathParsingException("Template paths starting with \"{\" are reseved for future purposes"); |
| } else { |
| templateLoader = new WebappTemplateLoader(srvCtx, pureTemplatePath); |
| } |
| |
| if (settingAssignmentsStart != -1) { |
| try { |
| int nextPos = _ObjectBuilderSettingEvaluator.configureBean( |
| templatePath, templatePath.indexOf('(', settingAssignmentsStart) + 1, templateLoader, |
| _SettingEvaluationEnvironment.getCurrent()); |
| if (nextPos != templatePath.length()) { |
| throw new TemplatePathParsingException("Template path should end after the setting list in: " |
| + templatePath); |
| } |
| } catch (Exception e) { |
| throw new TemplatePathParsingException("Failed to set properties in: " + templatePath, e); |
| } |
| } |
| |
| return templateLoader; |
| } |
| |
| static String normalizeToAbsolutePackagePath(String path) { |
| while (path.startsWith("/")) { |
| path = path.substring(1); |
| } |
| return "/" + path; |
| } |
| |
| static List/*<String>*/ parseCommaSeparatedList(String value) throws ParseException { |
| List/*<String>*/ valuesList = new ArrayList(); |
| String[] values = StringUtil.split(value, ','); |
| for (int i = 0; i < values.length; i++) { |
| final String s = values[i].trim(); |
| if (s.length() != 0) { |
| valuesList.add(s); |
| } else if (i != values.length - 1) { |
| throw new ParseException("Missing list item berfore a comma", -1); |
| } |
| } |
| return valuesList; |
| } |
| |
| static List parseCommaSeparatedPatterns(String value) throws ParseException { |
| List/*<String>*/ values = parseCommaSeparatedList(value); |
| List/*<Pattern>*/ patterns = new ArrayList(values.size()); |
| for (int i = 0; i < values.size(); i++) { |
| patterns.add(Pattern.compile((String) values.get(i))); |
| } |
| return patterns; |
| } |
| |
| /** |
| * This is like {@link #parseCommaSeparatedList(String)}, but is not confused by commas inside |
| * {@code ?settings(...)} parts at the end of the items. |
| */ |
| static List parseCommaSeparatedTemplatePaths(String commaSepItems) { |
| List listItems; |
| listItems = new ArrayList(); |
| while (commaSepItems.length() != 0) { |
| int itemSettingAssignmentsStart = findTemplatePathSettingAssignmentsStart(commaSepItems); |
| int pureItemEnd = itemSettingAssignmentsStart != -1 ? itemSettingAssignmentsStart : commaSepItems.length(); |
| int prevComaIdx = commaSepItems.lastIndexOf(',', pureItemEnd - 1); |
| int itemStart = prevComaIdx != -1 ? prevComaIdx + 1 : 0; |
| final String item = commaSepItems.substring(itemStart).trim(); |
| if (item.length() != 0) { |
| listItems.add(0, item); |
| } else if (listItems.size() > 0) { |
| throw new TemplatePathParsingException("Missing list item before a comma"); |
| } |
| commaSepItems = prevComaIdx != -1 ? commaSepItems.substring(0, prevComaIdx).trim() : ""; |
| } |
| return listItems; |
| } |
| |
| /** |
| * @return -1 if there's no setting assignment. |
| */ |
| static int findTemplatePathSettingAssignmentsStart(String s) { |
| int pos = s.length() - 1; |
| |
| // Skip WS |
| while (pos >= 0 && Character.isWhitespace(s.charAt(pos))) { |
| pos--; |
| } |
| |
| // Skip `)` |
| if (pos < 0 || s.charAt(pos) != ')') return -1; |
| pos--; |
| |
| // Skip `(...` |
| int parLevel = 1; |
| int mode = 0; |
| while (parLevel > 0) { |
| if (pos < 0) return -1; |
| char c = s.charAt(pos); |
| switch (mode) { |
| case 0: // 0: outside string literal |
| switch (c) { |
| case '(': parLevel--; break; |
| case ')': parLevel++; break; |
| case '\'': mode = 1; break; |
| case '"': mode = 2; break; |
| } |
| break; |
| case 1: // 1: inside '...' |
| if (c == '\'' && !(pos > 0 && s.charAt(pos - 1) == '\\')) { |
| mode = 0; |
| } |
| break; |
| case 2: // 2: inside "..." |
| if (c == '"' && !(pos > 0 && s.charAt(pos - 1) == '\\')) { |
| mode = 0; |
| } |
| break; |
| } |
| pos--; |
| } |
| |
| // Skip WS |
| while (pos >= 0 && Character.isWhitespace(s.charAt(pos))) { |
| pos--; |
| } |
| |
| int biNameEnd = pos + 1; |
| |
| // Skip name chars |
| while (pos >= 0 && Character.isJavaIdentifierPart(s.charAt(pos))) { |
| pos--; |
| } |
| |
| int biNameStart = pos + 1; |
| if (biNameStart == biNameEnd) { |
| return -1; |
| } |
| String biName = s.substring(biNameStart, biNameEnd); |
| |
| // Skip WS |
| while (pos >= 0 && Character.isWhitespace(s.charAt(pos))) { |
| pos--; |
| } |
| |
| // Skip `?` |
| if (pos < 0 || s.charAt(pos) != '?') return -1; |
| |
| if (!biName.equals(TEMPLATE_PATH_SETTINGS_BI_NAME)) { |
| throw new TemplatePathParsingException( |
| StringUtil.jQuote(biName) + " is unexpected after the \"?\". " |
| + "Expected \"" + TEMPLATE_PATH_SETTINGS_BI_NAME + "\"."); |
| } |
| |
| return pos; |
| } |
| |
| private static final class TemplatePathParsingException extends RuntimeException { |
| |
| public TemplatePathParsingException(String message, Throwable cause) { |
| super(message, cause); |
| } |
| |
| public TemplatePathParsingException(String message) { |
| super(message); |
| } |
| |
| } |
| |
| } |