http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java new file mode 100644 index 0000000..0a3b33a --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java @@ -0,0 +1,112 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Locale; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy; + +/** + * Used as the parameter of {@link TemplateLookupStrategy#lookup(TemplateLookupContext)}. + * You can't invoke instances of this, only receive them from FreeMarker. + * + * @since 2.3.22 + */ +public abstract class TemplateLookupContext<R extends TemplateLookupResult> { + + private final String templateName; + private final Locale templateLocale; + private final Object customLookupCondition; + + /** + * Finds the template source based on its <em>normalized</em> name; handles {@code *} steps (so called acquisition), + * otherwise it just calls {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, + * TemplateLoaderSession)}. + * + * @param templateName + * Must be a normalized name, like {@code "foo/bar/baaz.ftl"}. A name is not normalized when, among + * others, it starts with {@code /}, or contains {@code .} or {@code ..} path steps, or it uses + * backslash ({@code \}) instead of {@code /}. A normalized name might contains "*" path steps + * (acquisition). + * + * @return The result of the lookup. Not {@code null}; check {@link TemplateLookupResult#isPositive()} to see if the + * lookup has found anything. Note that in a positive result the content of the template is possibly + * also already loaded (this is the case for {@link TemplateLoader}-s when the cached content is stale, but + * not for {@link TemplateLoader}-s). Hence discarding a positive result and looking for another can + * generate substantial extra I/O. + */ + public abstract R lookupWithAcquisitionStrategy(String templateName) throws IOException; + + /** + * Finds the template source based on its <em>normalized</em> name; tries localized variations going from most + * specific to less specific, and for each variation it delegates to {@link #lookupWithAcquisitionStrategy(String)}. + * If {@code templateLocale} is {@code null} (typically, because {@link Configuration#getLocalizedLookup()} is + * {@code false})), then it's the same as calling {@link #lookupWithAcquisitionStrategy(String)} directly. This is + * the default strategy of FreeMarker (at least in 2.3.x), so for more information, see + * {@link DefaultTemplateLookupStrategy#INSTANCE}. + */ + public abstract R lookupWithLocalizedThenAcquisitionStrategy(String templateName, + Locale templateLocale) throws IOException; + + /** Default visibility to prevent extending the class from outside this package. */ + protected TemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition) { + this.templateName = templateName; + this.templateLocale = templateLocale; + this.customLookupCondition = customLookupCondition; + } + + /** + * The normalized name (path) of the template (relatively to the {@link TemplateLoader}). Not {@code null}. + */ + public String getTemplateName() { + return templateName; + } + + /** + * {@code null} if localized lookup is disabled (see {@link Configuration#getLocalizedLookup()}), otherwise the + * locale requested. + */ + public Locale getTemplateLocale() { + return templateLocale; + } + + /** + * Returns the value of the {@code customLookupCondition} parameter of + * {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}; see requirements there, such + * as having a proper {@link Object#equals(Object)} and {@link Object#hashCode()} method. The interpretation of this + * value is up to the custom {@link TemplateLookupStrategy}. Usually, it's used similarly to as the default lookup + * strategy uses {@link #getTemplateLocale()}, that is, to look for a template variation that satisfies the + * condition, and then maybe fall back to more generic template if that's missing. + */ + public Object getCustomLookupCondition() { + return customLookupCondition; + } + + /** + * Creates a not-found lookup result that then can be used as the return value of + * {@link TemplateLookupStrategy#lookup(TemplateLookupContext)}. (In the current implementation it just always + * returns the same static singleton, but that might need to change in the future.) + */ + public abstract R createNegativeLookupResult(); + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java new file mode 100644 index 0000000..d9a2594 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java @@ -0,0 +1,54 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.templateresolver.impl.TemplateLoaderBasedTemplateLookupResult; + +/** + * The return value of {@link TemplateLookupStrategy#lookup(TemplateLookupContext)} and similar lookup methods. You + * usually get one from {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)} or + * {@link TemplateLookupContext#createNegativeLookupResult()}. + * + * <p> + * Subclass this only if you are implementing a {@link TemplateLookupContext}; if the {@link TemplateLookupContext} that + * you are implementing uses {@link TemplateLoader}-s, consider using {@link TemplateLoaderBasedTemplateLookupResult} + * instead of writing your own subclass. + * + * @since 2.3.22 + */ +public abstract class TemplateLookupResult { + + protected TemplateLookupResult() { + // nop + } + + /** + * The source name of the template found (see {@link Template#getSourceName()}), or {@code null} if + * {@link #isPositive()} is {@code false}. + */ + public abstract String getTemplateSourceName(); + + /** + * Tells if the lookup has found a matching template. + */ + public abstract boolean isPositive(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java new file mode 100644 index 0000000..7021b5b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java @@ -0,0 +1,78 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +import java.io.IOException; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Template; + +/** + * Finds the {@link TemplateLoader}-level (storage-level) template source for the template name with which the template + * was requested (as in {@link Configuration#getTemplate(String)}). This usually means trying various + * {@link TemplateLoader}-level template names (so called source names; see also {@link Template#getSourceName()}) that + * were deduced from the requested name. Trying a name usually means calling + * {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)} with it and checking the value of + * {@link TemplateLookupResult#isPositive()}. + * + * <p> + * Before you write your own lookup strategy, know that: + * <ul> + * <li>A template lookup strategy meant to operate solely with template names, not with {@link TemplateLoader}-s + * directly. Basically, it's a mapping between the template names that templates and API-s like + * {@link Configuration#getTemplate(String)} see, and those that the underlying {@link TemplateLoader} sees. + * <li>A template lookup strategy doesn't influence the template's name ({@link Template#getLookupName()}), which is the + * normalized form of the template name as it was requested (with {@link Configuration#getTemplate(String)}, etc.). It + * only influences the so called source name of the template ({@link Template#getSourceName()}). The template's name is + * used as the basis for resolving relative inclusions/imports in the template. The source name is pretty much only used + * in error messages as error location, and of course, to actually load the template "file". + * <li>Understand the impact of the last point if your template lookup strategy fiddles not only with the file name part + * of the template name, but also with the directory part. For example, one may want to map "foo.ftl" to "en/foo.ftl", + * "fr/foo.ftl", etc. That's legal, but the result is kind of like if you had several root directories ("en/", "fr/", + * etc.) that are layered over each other to form a single merged directory. (This is what's desirable in typical + * applications, yet it can be confusing.) + * </ul> + * + * @see Configuration#getTemplateLookupStrategy() + * + * @since 2.3.22 + */ +public abstract class TemplateLookupStrategy { + + /** + * Finds the template source that matches the template name, locale (if not {@code null}) and other parameters + * specified in the {@link TemplateLookupContext}. See also the class-level {@link TemplateLookupStrategy} + * documentation to understand lookup strategies more. + * + * @param ctx + * Contains the parameters for which the matching template need to be found, and operations that + * are needed to implement the strategy. Some of the important input parameters are: + * {@link TemplateLookupContext#getTemplateName()}, {@link TemplateLookupContext#getTemplateLocale()}. + * The most important operations are {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)} + * and {@link TemplateLookupContext#createNegativeLookupResult()}. (Note that you deliberately can't + * use {@link TemplateLoader}-s directly to implement lookup.) + * + * @return Usually the return value of {@link TemplateLookupContext#lookupWithAcquisitionStrategy(String)}, or + * {@code TemplateLookupContext#createNegativeLookupResult()} if no matching template exists. Can't be + * {@code null}. + */ + public abstract <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> ctx) throws IOException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java new file mode 100644 index 0000000..57773f4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java @@ -0,0 +1,53 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +/** + * Symbolizes a template name format, which defines the basic syntax of names through algorithms such as normalization. + */ +// TODO [FM3] Before it becomes a BC problem, shouldn't we add methods like splitting to directory name and file name? +public abstract class TemplateNameFormat { + + protected TemplateNameFormat() { + // + } + + /** + * Implements {@link TemplateResolver#toRootBasedName(String, String)}; see more there. + */ + public abstract String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException; + + /** + * Implements {@link TemplateResolver#normalizeRootBasedName(String)}; see more there. + */ + public abstract String normalizeRootBasedName(String name) throws MalformedTemplateNameException; + + protected void checkNameHasNoNullCharacter(final String name) throws MalformedTemplateNameException { + if (name.indexOf(0) != -1) { + throw new MalformedTemplateNameException(name, + "Null character (\\u0000) in the name; possible attack attempt"); + } + } + + protected MalformedTemplateNameException newRootLeavingException(final String name) { + return new MalformedTemplateNameException(name, "Backing out from the root directory is not allowed"); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java new file mode 100644 index 0000000..bf7280a --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java @@ -0,0 +1,166 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Locale; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.ParseException; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.TemplateNotFoundException; +import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver; + +/** + * This class was introduced to allow users to fully implement the template lookup, loading and caching logic, + * in case the standard mechanism ({@link DefaultTemplateResolver}) is not flexible enough. By implementing this class, + * you can take over the duty of the following {@link Configuration} settings, and it's up to the implementation if you + * delegate some of those duties back to the {@link Configuration} setting: + * + * <ul> + * <li>{@link Configuration#getTemplateLoader() templateLoader} + * <li>{@link Configuration#getTemplateNameFormat() templateNameFormat} + * <li>{@link Configuration#getTemplateLookupStrategy() templateLookupStrategy} + * <li>{@link Configuration#getCacheStorage() cacheStorage} + * </ul> + * + * @since 3.0.0 + */ +//TODO DRAFT only [FM3] +public abstract class TemplateResolver { + + private final Configuration configuration; + + protected TemplateResolver(Configuration configuration) { + this.configuration = configuration; + } + + public Configuration getConfiguration() { + return configuration; + } + + /** + * Retrieves the parsed template with the given name (and according the specified further parameters), or returns a + * result that indicates that no such template exists. The result should come from a cache most of the time + * (avoiding I/O and template parsing), as this method is typically called frequently. + * + * <p> + * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters + * see {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}. + * + * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a + * {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information + * about the missing template. The return value itself is never {@code null}. Note that exceptions occurring + * during template loading mustn't be treated as a missing template, they must cause an exception to be + * thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a + * missing template is normal (not exceptional), because of how some lookup strategies work. That the + * backing storage mechanism should indeed check that it's missing though, and not cover an error as such. + * + * @throws MalformedTemplateNameException + * If the {@code name} was malformed. This is certainly originally thrown by + * {@link #normalizeRootBasedName(String)}; see more there. + * + * @throws IOException + * If reading the template has failed from a reason other than the template is missing. This method + * should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return + * value. + */ + // [FM3] This parameters will be removed: String encoding + public abstract GetTemplateResult getTemplate(String name, Locale locale, Serializable customLookupCondition) + throws MalformedTemplateNameException, ParseException, IOException; + + /** + * Clears the cache of templates, to enforce re-loading templates when they are get next time; this is an optional + * operation. + * + * <p> + * Note that if the {@link TemplateResolver} implementation uses {@link TemplateLoader}-s, it should also call + * {@link TemplateLoader#resetState()} on them. + * + * <p> + * This method is thread-safe and can be called while the engine processes templates. + * + * @throws UnsupportedOperationException If the {@link TemplateResolver} implementation doesn't support this + * operation. + */ + public abstract void clearTemplateCache() throws UnsupportedOperationException; + + /** + * Removes a template from the template cache, hence forcing the re-loading of it when it's next time requested; + * this is an optional operation. This is to give the application finer control over cache updating than the + * {@link Configuration#getTemplateUpdateDelayMilliseconds() templateUpdateDelayMilliseconds} setting alone gives. + * + * <p> + * For the meaning of the parameters, see {@link #getTemplate(String, Locale, Serializable)} + * + * <p> + * This method is thread-safe and can be called while the engine processes templates. + * + * @throws UnsupportedOperationException If the {@link TemplateResolver} implementation doesn't support this + * operation. + */ + public abstract void removeTemplateFromCache(String name, Locale locale, Serializable customLookupCondition) + throws IOException, UnsupportedOperationException; + + /** + * Converts a name to a template root directory based name, so that it can be used to find a template without + * knowing what (like which template) has referred to it. The rules depend on the name format, but a typical example + * is converting "t.ftl" with base "sub/contex.ftl" to "sub/t.ftl". + * + * <p> + * Some implementations, notably {@link DefaultTemplateResolver}, delegates this check to the + * {@link TemplateNameFormat} coming from the {@link Configuration}. + * + * @param baseName + * Maybe a file name, maybe a directory name. The meaning of file name VS directory name depends on the + * name format, but typically, something like "foo/bar/" is a directory name, and something like + * "foo/bar" is a file name, and thus in the last case the effective base is "foo/" (i.e., the directory + * that contains the file). Not {@code null}. + * @param targetName + * The name to convert. This usually comes from a template that refers to another template by name. It + * can be a relative name, or an absolute name. (In typical name formats absolute names start with + * {@code "/"} or maybe with an URL scheme, and all others are relative). Not {@code null}. + * + * @return The path in template root directory relative format, or even an absolute name (where the root directory + * is not the real root directory of the file system, but the imaginary directory that exists to store the + * templates). The standard implementations shipped with FreeMarker always return a root relative path + * (except if the name starts with an URI schema, in which case a full URI is returned). + */ + public abstract String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException; + + /** + * Normalizes a template root directory based name (relative to the root or absolute), so that equivalent names + * become equivalent according {@link String#equals(Object)} too. The rules depend on the name format, but typical + * examples are "sub/../t.ftl" to "t.ftl", "sub/./t.ftl" to "sub/t.ftl" and "/t.ftl" to "t.ftl". + * + * <p> + * Some implementations, notably {@link DefaultTemplateResolver}, delegates this check to the {@link TemplateNameFormat} + * coming from the {@link Configuration}. The standard {@link TemplateNameFormat} implementations shipped with + * FreeMarker always returns a root relative path (except if the name starts with an URI schema, in which case a + * full URI is returned), for example, "/foo.ftl" becomes to "foo.ftl". + * + * @param name + * The root based name. Not {@code null}. + * + * @return The normalized root based name. Not {@code null}. + */ + public abstract String normalizeRootBasedName(String name) throws MalformedTemplateNameException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java new file mode 100644 index 0000000..ca38c39 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/TemplateSourceMatcher.java @@ -0,0 +1,30 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +import java.io.IOException; + +/** + * @since 2.3.24 + */ +public abstract class TemplateSourceMatcher { + + abstract boolean matches(String sourceName, Object templateSource) throws IOException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java new file mode 100644 index 0000000..09b216f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/_CacheAPI.java @@ -0,0 +1,43 @@ +/* + * 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 org.apache.freemarker.core.templateresolver; + +/** + * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! + * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can + * access things inside this package that users shouldn't. + */ +public final class _CacheAPI { + + private _CacheAPI() { + // Not meant to be instantiated + } + + public static String toRootBasedName(TemplateNameFormat templateNameFormat, String baseName, String targetName) + throws MalformedTemplateNameException { + return templateNameFormat.toRootBasedName(baseName, targetName); + } + + public static String normalizeRootBasedName(TemplateNameFormat templateNameFormat, String name) + throws MalformedTemplateNameException { + return templateNameFormat.normalizeRootBasedName(name); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java new file mode 100644 index 0000000..417566f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java @@ -0,0 +1,199 @@ +/* + * 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 org.apache.freemarker.core.templateresolver.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.freemarker.core.templateresolver.CacheStorage; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLoaderSession; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResult; +import org.apache.freemarker.core.templateresolver.TemplateLoadingSource; +import org.apache.freemarker.core.util._StringUtil; + +/** + * A {@link TemplateLoader} that uses a {@link Map} with {@code byte[]} as its source of templates. This is similar to + * {@link StringTemplateLoader}, but uses {@code byte[]} instead of {@link String}; see more details there. + * + * <p>Note that {@link ByteArrayTemplateLoader} can't be used with a distributed (cluster-wide) {@link CacheStorage}, + * as it produces {@link TemplateLoadingSource}-s that deliberately throw exception on serialization (because the + * content is only accessible within a single JVM, and is also volatile). + */ +// TODO JUnit tests +public class ByteArrayTemplateLoader implements TemplateLoader { + + private static final AtomicLong INSTANCE_COUNTER = new AtomicLong(); + + private final long instanceId = INSTANCE_COUNTER.get(); + private final AtomicLong templatesRevision = new AtomicLong(); + private final ConcurrentMap<String, ContentHolder> templates = new ConcurrentHashMap<>(); + + /** + * Puts a template into the template loader. The name can contain slashes to denote logical directory structure, but + * must not start with a slash. Each template will get an unique revision number, thus replacing a template will + * cause the template cache to reload it (when the update delay expires). + * + * <p>This method is thread-safe. + * + * @param name + * the name of the template. + * @param content + * the source code of the template. + */ + public void putTemplate(String name, byte[] content) { + templates.put( + name, + new ContentHolder(content, new Source(instanceId, name), templatesRevision.incrementAndGet())); + } + + /** + * Removes the template with the specified name if it was added earlier. + * + * <p> + * This method is thread-safe. + * + * @param name + * Exactly the key with which the template was added. + * + * @return Whether a template was found with the given key (and hence was removed now) + */ + public boolean removeTemplate(String name) { + return templates.remove(name) != null; + } + + @Override + public TemplateLoaderSession createSession() { + return null; + } + + @Override + public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, + Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException { + ContentHolder contentHolder = templates.get(name); + if (contentHolder == null) { + return TemplateLoadingResult.NOT_FOUND; + } else if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(contentHolder.source) + && Objects.equals(ifVersionDiffersFrom, contentHolder.version)) { + return TemplateLoadingResult.NOT_MODIFIED; + } else { + return new TemplateLoadingResult( + contentHolder.source, contentHolder.version, + new ByteArrayInputStream(contentHolder.content), + null); + } + } + + @Override + public void resetState() { + // Do nothing + } + + /** + * Show class name and some details that are useful in template-not-found errors. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(_TemplateLoaderUtils.getClassNameForToString(this)); + sb.append("(Map { "); + int cnt = 0; + for (String name : templates.keySet()) { + cnt++; + if (cnt != 1) { + sb.append(", "); + } + if (cnt > 10) { + sb.append("..."); + break; + } + sb.append(_StringUtil.jQuote(name)); + sb.append("=..."); + } + if (cnt != 0) { + sb.append(' '); + } + sb.append("})"); + return sb.toString(); + } + + private static class ContentHolder { + private final byte[] content; + private final Source source; + private final long version; + + public ContentHolder(byte[] content, Source source, long version) { + this.content = content; + this.source = source; + this.version = version; + } + + } + + @SuppressWarnings("serial") + private static class Source implements TemplateLoadingSource { + + private final long instanceId; + private final String name; + + public Source(long instanceId, String name) { + this.instanceId = instanceId; + this.name = name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (instanceId ^ (instanceId >>> 32)); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Source other = (Source) obj; + if (instanceId != other.instanceId) return false; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + private void writeObject(ObjectOutputStream out) throws IOException { + throw new IOException(ByteArrayTemplateLoader.class.getName() + + " sources can't be serialized, as they don't support clustering."); + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java new file mode 100644 index 0000000..331307f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java @@ -0,0 +1,184 @@ +/* + * 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 org.apache.freemarker.core.templateresolver.impl; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; + +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResult; +import org.apache.freemarker.core.util._NullArgumentException; +import org.apache.freemarker.core.util._StringUtil; + +/** + * A {@link TemplateLoader} that can load templates from the "classpath". Naturally, it can load from jar files, or from + * anywhere where Java can load classes from. Internally, it uses {@link Class#getResource(String)} or + * {@link ClassLoader#getResource(String)} to load templates. + */ +// TODO +public class ClassTemplateLoader extends URLTemplateLoader { + + private final Class<?> resourceLoaderClass; + private final ClassLoader classLoader; + private final String basePackagePath; + + /** + * Creates a template loader that will use the {@link Class#getResource(String)} method of the specified class to + * load the resources, and the specified base package path (absolute or relative). + * + * <p> + * Examples: + * <ul> + * <li>Relative base path (will load from the {@code com.example.myapplication.templates} package):<br> + * {@code new ClassTemplateLoader(com.example.myapplication.SomeClass.class, "templates")} + * <li>Absolute base path:<br> + * {@code new ClassTemplateLoader(somepackage.SomeClass.class, "/com/example/myapplication/templates")} + * </ul> + * + * @param resourceLoaderClass + * The class whose {@link Class#getResource(String)} method will be used to load the templates. Be sure + * that you chose a class whose defining class-loader sees the templates. This parameter can't be + * {@code null}. + * @param basePackagePath + * The package that contains the templates, in path ({@code /}-separated) format. If it doesn't start + * with a {@code /} then it's relative to the path (package) of the {@code resourceLoaderClass} class. If + * it starts with {@code /} then it's relative to the root of the package hierarchy. Note that path + * components should be separated by forward slashes independently of the separator character used by the + * underlying operating system. This parameter can't be {@code null}. + * + * @see #ClassTemplateLoader(ClassLoader, String) + */ + public ClassTemplateLoader(Class<?> resourceLoaderClass, String basePackagePath) { + this(resourceLoaderClass, false, null, basePackagePath); + } + + /** + * Similar to {@link #ClassTemplateLoader(Class, String)}, but instead of {@link Class#getResource(String)} it uses + * {@link ClassLoader#getResource(String)}. Because a {@link ClassLoader} isn't bound to any Java package, it + * doesn't mater if the {@code basePackagePath} starts with {@code /} or not, it will be always relative to the root + * of the package hierarchy + */ + public ClassTemplateLoader(ClassLoader classLoader, String basePackagePath) { + this(null, true, classLoader, basePackagePath); + } + + private ClassTemplateLoader(Class<?> resourceLoaderClass, boolean allowNullResourceLoaderClass, + ClassLoader classLoader, String basePackagePath) { + if (!allowNullResourceLoaderClass) { + _NullArgumentException.check("resourceLoaderClass", resourceLoaderClass); + } + _NullArgumentException.check("basePackagePath", basePackagePath); + + // Either set a non-null resourceLoaderClass or a non-null classLoader, not both: + this.resourceLoaderClass = classLoader == null ? (resourceLoaderClass == null ? getClass() + : resourceLoaderClass) : null; + if (this.resourceLoaderClass == null && classLoader == null) { + throw new _NullArgumentException("classLoader"); + } + this.classLoader = classLoader; + + String canonBasePackagePath = canonicalizePrefix(basePackagePath); + if (this.classLoader != null && canonBasePackagePath.startsWith("/")) { + canonBasePackagePath = canonBasePackagePath.substring(1); + } + this.basePackagePath = canonBasePackagePath; + } + + private static boolean isSchemeless(String fullPath) { + int i = 0; + int ln = fullPath.length(); + + // Skip a single initial /, as things like "/file:/..." might work: + if (i < ln && fullPath.charAt(i) == '/') i++; + + // Check if there's no ":" earlier than a '/', as the URLClassLoader + // could interpret that as an URL scheme: + while (i < ln) { + char c = fullPath.charAt(i); + if (c == '/') return true; + if (c == ':') return false; + i++; + } + return true; + } + + /** + * Show class name and some details that are useful in template-not-found errors. + */ + @Override + public String toString() { + return _TemplateLoaderUtils.getClassNameForToString(this) + "(" + + (resourceLoaderClass != null + ? "resourceLoaderClass=" + resourceLoaderClass.getName() + : "classLoader=" + _StringUtil.jQuote(classLoader)) + + ", basePackagePath" + + "=" + + _StringUtil.jQuote(basePackagePath) + + (resourceLoaderClass != null + ? (basePackagePath.startsWith("/") ? "" : " /* relatively to resourceLoaderClass pkg */") + : "" + ) + + ")"; + } + + /** + * See the similar parameter of {@link #ClassTemplateLoader(Class, String)}; {@code null} when other mechanism is + * used to load the resources. + */ + public Class<?> getResourceLoaderClass() { + return resourceLoaderClass; + } + + /** + * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, String)}; {@code null} when other mechanism + * is used to load the resources. + */ + public ClassLoader getClassLoader() { + return classLoader; + } + + /** + * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, String)}; note that this is a normalized + * version of what was actually passed to the constructor. + */ + public String getBasePackagePath() { + return basePackagePath; + } + + @Override + protected URL getURL(String name) { + String fullPath = basePackagePath + name; + + // Block java.net.URLClassLoader exploits: + if (basePackagePath.equals("/") && !isSchemeless(fullPath)) { + return null; + } + + return resourceLoaderClass != null ? resourceLoaderClass.getResource(fullPath) : classLoader + .getResource(fullPath); + } + + @Override + protected TemplateLoadingResult extractNegativeResult(URLConnection conn) throws IOException { + return null; + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java new file mode 100644 index 0000000..185f5b9 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java @@ -0,0 +1,61 @@ +/* + * 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 org.apache.freemarker.core.templateresolver.impl; + +import java.io.IOException; +import java.util.Locale; + +import org.apache.freemarker.core.templateresolver.TemplateLookupContext; +import org.apache.freemarker.core.templateresolver.TemplateLookupResult; +import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; + +/** + * <p> + * The default lookup strategy of FreeMarker. + * + * <p> + * Through an example: Assuming localized lookup is enabled and that a template is requested for the name + * {@code example.ftl} and {@code Locale("es", "ES", "Traditional_WIN")}, it will try the following template names, + * in this order: {@code "foo_en_AU_Traditional_WIN.ftl"}, {@code "foo_en_AU_Traditional.ftl"}, + * {@code "foo_en_AU.ftl"}, {@code "foo_en.ftl"}, {@code "foo.ftl"}. It stops at the first variation where it finds + * a template. (If the template name contains "*" steps, finding the template for the attempted localized variation + * happens with the template acquisition mechanism.) If localized lookup is disabled, it won't try to add any locale + * strings, so it just looks for {@code "foo.ftl"}. + * + * <p> + * The generation of the localized name variation with the default lookup strategy, happens like this: It removes + * the file extension (the part starting with the <em>last</em> dot), then appends {@link Locale#toString()} after + * it, and puts back the extension. Then it starts to remove the parts from the end of the locale, considering + * {@code "_"} as the separator between the parts. It won't remove parts that are not part of the locale string + * (like if the requested template name is {@code foo_bar.ftl}, it won't remove the {@code "_bar"}). + */ +public class DefaultTemplateLookupStrategy extends TemplateLookupStrategy { + + public static final DefaultTemplateLookupStrategy INSTANCE = new DefaultTemplateLookupStrategy(); + + private DefaultTemplateLookupStrategy() { + // + } + + @Override + public <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> ctx) throws IOException { + return ctx.lookupWithLocalizedThenAcquisitionStrategy(ctx.getTemplateName(), ctx.getTemplateLocale()); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java new file mode 100644 index 0000000..69fa390 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java @@ -0,0 +1,309 @@ +/* + * 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 org.apache.freemarker.core.templateresolver.impl; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.TemplateNotFoundException; +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateNameFormat; +import org.apache.freemarker.core.util._StringUtil; + +/** + * The default template name format only when {@link Configuration#getIncompatibleImprovements() + * incompatible_improvements} is set to 2.4.0 (or higher). This is not the out-of-the-box default format of FreeMarker + * 2.4.x, because the default {@code incompatible_improvements} is still 2.3.0 there. + * + * <p> + * Differences to the {@link DefaultTemplateNameFormatFM2} format: + * + * <ul> + * + * <li>The scheme and the path need not be separated with {@code "://"} anymore, only with {@code ":"}. This makes + * template names like {@code "classpath:foo.ftl"} interpreted as an absolute name with scheme {@code "classpath"} + * and absolute path "foo.ftl". The scheme name before the {@code ":"} can't contain {@code "/"}, or else it's + * treated as a malformed name. The scheme part can be separated either with {@code "://"} or just {@code ":"} from + * the path. Hence, {@code myschme:/x} is normalized to {@code myschme:x}, while {@code myschme:///x} is normalized + * to {@code myschme://x}, but {@code myschme://x} or {@code myschme:/x} aren't changed by normalization. It's up + * the {@link TemplateLoader} to which the normalized names are passed to decide which of these scheme separation + * conventions are valid (maybe both).</li> + * + * <li>{@code ":"} is not allowed in template names, except as the scheme separator (see previous point). + * + * <li>Malformed paths throw {@link MalformedTemplateNameException} instead of acting like if the template wasn't + * found. + * + * <li>{@code "\"} (backslash) is not allowed in template names, and causes {@link MalformedTemplateNameException}. + * With {@link DefaultTemplateNameFormatFM2} you would certainly end up with a {@link TemplateNotFoundException} (or + * worse, it would work, but steps like {@code ".."} wouldn't be normalized by FreeMarker). + * + * <li>Template names might end with {@code /}, like {@code "foo/"}, and the presence or lack of the terminating + * {@code /} is seen as significant. While their actual interpretation is up to the {@link TemplateLoader}, + * operations that manipulate template names assume that the last step refers to a "directory" as opposed to a + * "file" exactly if the terminating {@code /} is present. Except, the empty name is assumed to refer to the root + * "directory" (despite that it doesn't end with {@code /}). + * + * <li>{@code //} is normalized to {@code /}, except of course if it's in the scheme name terminator. Like + * {@code foo//bar///baaz.ftl} is normalized to {@code foo/bar/baaz.ftl}. (In general, 0 long step names aren't + * possible anymore.)</li> + * + * <li>The {@code ".."} bugs of the legacy normalizer are oms: {@code ".."} steps has removed the preceding + * {@code "."} or {@code "*"} or scheme steps, not treating them specially as they should be. Now these work as + * expected. Examples: {@code "a/./../c"} has become to {@code "a/c"}, now it will be {@code "c"}; {@code "a/b/*} + * {@code /../c"} has become to {@code "a/b/c"}, now it will be {@code "a/*}{@code /c"}; {@code "scheme://.."} has + * become to {@code "scheme:/"}, now it will be {@code null} ({@link TemplateNotFoundException}) for backing out of + * the root directory.</li> + * + * <li>As now directory paths has to be handled as well, it recognizes terminating, leading, and lonely {@code ".."} + * and {@code "."} steps. For example, {@code "foo/bar/.."} now becomes to {@code "foo/"}</li> + * + * <li>Multiple consecutive {@code *} steps are normalized to one</li> + * + * </ul> + */ +public final class DefaultTemplateNameFormat extends TemplateNameFormat { + + public static DefaultTemplateNameFormat INSTANCE = new DefaultTemplateNameFormat(); + + private DefaultTemplateNameFormat() { + // + } + + @Override + public String toRootBasedName(String baseName, String targetName) { + if (findSchemeSectionEnd(targetName) != 0) { + return targetName; + } else if (targetName.startsWith("/")) { // targetName is an absolute path + final String targetNameAsRelative = targetName.substring(1); + final int schemeSectionEnd = findSchemeSectionEnd(baseName); + if (schemeSectionEnd == 0) { + return targetNameAsRelative; + } else { + // Prepend the scheme of baseName: + return baseName.substring(0, schemeSectionEnd) + targetNameAsRelative; + } + } else { // targetName is a relative path + if (!baseName.endsWith("/")) { + // Not a directory name => get containing directory name + int baseEnd = baseName.lastIndexOf("/") + 1; + if (baseEnd == 0) { + // For something like "classpath:t.ftl", must not remove the scheme part: + baseEnd = findSchemeSectionEnd(baseName); + } + baseName = baseName.substring(0, baseEnd); + } + return baseName + targetName; + } + } + + @Override + public String normalizeRootBasedName(final String name) throws MalformedTemplateNameException { + // Disallow 0 for security reasons. + checkNameHasNoNullCharacter(name); + + if (name.indexOf('\\') != -1) { + throw new MalformedTemplateNameException( + name, + "Backslash (\"\\\") is not allowed in template names. Use slash (\"/\") instead."); + } + + // Split name to a scheme and a path: + final String scheme; + String path; + { + int schemeSectionEnd = findSchemeSectionEnd(name); + if (schemeSectionEnd == 0) { + scheme = null; + path = name; + } else { + scheme = name.substring(0, schemeSectionEnd); + path = name.substring(schemeSectionEnd); + } + } + + if (path.indexOf(':') != -1) { + throw new MalformedTemplateNameException(name, + "The ':' character can only be used after the scheme name (if there's any), " + + "not in the path part"); + } + + path = removeRedundantSlashes(path); + // path now doesn't start with "/" + + path = removeDotSteps(path); + + path = resolveDotDotSteps(path, name); + + path = removeRedundantStarSteps(path); + + return scheme == null ? path : scheme + path; + } + + private int findSchemeSectionEnd(String name) { + int schemeColonIdx = name.indexOf(":"); + if (schemeColonIdx == -1 || name.lastIndexOf('/', schemeColonIdx - 1) != -1) { + return 0; + } else { + // If there's a following "//", it's treated as the part of the scheme section: + if (schemeColonIdx + 2 < name.length() + && name.charAt(schemeColonIdx + 1) == '/' && name.charAt(schemeColonIdx + 2) == '/') { + return schemeColonIdx + 3; + } else { + return schemeColonIdx + 1; + } + } + } + + private String removeRedundantSlashes(String path) { + String prevName; + do { + prevName = path; + path = _StringUtil.replace(path, "//", "/"); + } while (prevName != path); + return path.startsWith("/") ? path.substring(1) : path; + } + + private String removeDotSteps(String path) { + int nextFromIdx = path.length() - 1; + findDotSteps: while (true) { + final int dotIdx = path.lastIndexOf('.', nextFromIdx); + if (dotIdx < 0) { + return path; + } + nextFromIdx = dotIdx - 1; + + if (dotIdx != 0 && path.charAt(dotIdx - 1) != '/') { + // False alarm + continue findDotSteps; + } + + final boolean slashRight; + if (dotIdx + 1 == path.length()) { + slashRight = false; + } else if (path.charAt(dotIdx + 1) == '/') { + slashRight = true; + } else { + // False alarm + continue findDotSteps; + } + + if (slashRight) { // "foo/./bar" or "./bar" + path = path.substring(0, dotIdx) + path.substring(dotIdx + 2); + } else { // "foo/." or "." + path = path.substring(0, path.length() - 1); + } + } + } + + /** + * @param name The original name, needed for exception error messages. + */ + private String resolveDotDotSteps(String path, final String name) throws MalformedTemplateNameException { + int nextFromIdx = 0; + findDotDotSteps: while (true) { + final int dotDotIdx = path.indexOf("..", nextFromIdx); + if (dotDotIdx < 0) { + return path; + } + + if (dotDotIdx == 0) { + throw newRootLeavingException(name); + } else if (path.charAt(dotDotIdx - 1) != '/') { + // False alarm + nextFromIdx = dotDotIdx + 3; + continue findDotDotSteps; + } + // Here we know that it has a preceding "/". + + final boolean slashRight; + if (dotDotIdx + 2 == path.length()) { + slashRight = false; + } else if (path.charAt(dotDotIdx + 2) == '/') { + slashRight = true; + } else { + // False alarm + nextFromIdx = dotDotIdx + 3; + continue findDotDotSteps; + } + + int previousSlashIdx; + boolean skippedStarStep = false; + { + int searchSlashBacwardsFrom = dotDotIdx - 2; // before the "/.." + scanBackwardsForSlash: while (true) { + if (searchSlashBacwardsFrom == -1) { + throw newRootLeavingException(name); + } + previousSlashIdx = path.lastIndexOf('/', searchSlashBacwardsFrom); + if (previousSlashIdx == -1) { + if (searchSlashBacwardsFrom == 0 && path.charAt(0) == '*') { + // "*/.." + throw newRootLeavingException(name); + } + break scanBackwardsForSlash; + } + if (path.charAt(previousSlashIdx + 1) == '*' && path.charAt(previousSlashIdx + 2) == '/') { + skippedStarStep = true; + searchSlashBacwardsFrom = previousSlashIdx - 1; + } else { + break scanBackwardsForSlash; + } + } + } + + // Note: previousSlashIdx is possibly -1 + // Removed part in {}: "a/{b/*/../}c" or "a/{b/*/..}" + path = path.substring(0, previousSlashIdx + 1) + + (skippedStarStep ? "*/" : "") + + path.substring(dotDotIdx + (slashRight ? 3 : 2)); + nextFromIdx = previousSlashIdx + 1; + } + } + + private String removeRedundantStarSteps(String path) { + String prevName; + removeDoubleStarSteps: do { + int supiciousIdx = path.indexOf("*/*"); + if (supiciousIdx == -1) { + break removeDoubleStarSteps; + } + + prevName = path; + + // Is it delimited on both sided by "/" or by the string boundaires? + if ((supiciousIdx == 0 || path.charAt(supiciousIdx - 1) == '/') + && (supiciousIdx + 3 == path.length() || path.charAt(supiciousIdx + 3) == '/')) { + path = path.substring(0, supiciousIdx) + path.substring(supiciousIdx + 2); + } + } while (prevName != path); + + // An initial "*" step is redundant: + if (path.startsWith("*")) { + if (path.length() == 1) { + path = ""; + } else if (path.charAt(1) == '/') { + path = path.substring(2); + } + // else: it's wasn't a "*" step. + } + + return path; + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java new file mode 100644 index 0000000..c5db8e5 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java @@ -0,0 +1,105 @@ +/* + * 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 org.apache.freemarker.core.templateresolver.impl; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.templateresolver.TemplateNameFormat; + +/** + * The default template name format when {@link Configuration#getIncompatibleImprovements() incompatible_improvements} + * is below 2.4.0. As of FreeMarker 2.4.0, the default {@code incompatible_improvements} is still {@code 2.3.0}, and it + * will certainly remain so for a very long time. In new projects it's highly recommended to use + * {@link DefaultTemplateNameFormat#INSTANCE} instead. + * + * @deprecated [FM3] Remove + */ +@Deprecated +public final class DefaultTemplateNameFormatFM2 extends TemplateNameFormat { + + public static final DefaultTemplateNameFormatFM2 INSTANCE = new DefaultTemplateNameFormatFM2(); + + private DefaultTemplateNameFormatFM2() { + // + } + + @Override + public String toRootBasedName(String baseName, String targetName) { + if (targetName.indexOf("://") > 0) { + return targetName; + } else if (targetName.startsWith("/")) { + int schemeSepIdx = baseName.indexOf("://"); + if (schemeSepIdx > 0) { + return baseName.substring(0, schemeSepIdx + 2) + targetName; + } else { + return targetName.substring(1); + } + } else { + if (!baseName.endsWith("/")) { + baseName = baseName.substring(0, baseName.lastIndexOf("/") + 1); + } + return baseName + targetName; + } + } + + @Override + public String normalizeRootBasedName(final String name) throws MalformedTemplateNameException { + // Disallow 0 for security reasons. + checkNameHasNoNullCharacter(name); + + // The legacy algorithm haven't considered schemes, so the name is in effect a path. + // Also, note that `path` will be repeatedly replaced below, while `name` is final. + String path = name; + + for (; ; ) { + int parentDirPathLoc = path.indexOf("/../"); + if (parentDirPathLoc == 0) { + // If it starts with /../, then it reaches outside the template + // root. + throw newRootLeavingException(name); + } + if (parentDirPathLoc == -1) { + if (path.startsWith("../")) { + throw newRootLeavingException(name); + } + break; + } + int previousSlashLoc = path.lastIndexOf('/', parentDirPathLoc - 1); + path = path.substring(0, previousSlashLoc + 1) + + path.substring(parentDirPathLoc + "/../".length()); + } + for (; ; ) { + int currentDirPathLoc = path.indexOf("/./"); + if (currentDirPathLoc == -1) { + if (path.startsWith("./")) { + path = path.substring("./".length()); + } + break; + } + path = path.substring(0, currentDirPathLoc) + + path.substring(currentDirPathLoc + "/./".length() - 1); + } + // Editing can leave us with a leading slash; strip it. + if (path.length() > 1 && path.charAt(0) == '/') { + path = path.substring(1); + } + return path; + } + +} \ No newline at end of file
