blob: 16137e77487a8b14af791abe5ee98ba6dc193b8f [file] [log] [blame]
/*
* 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.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Objects;
import org.apache.freemarker.core.Configuration;
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.templateresolver.TemplateLookupStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an abstract template loader that can load templates whose location can be described by an URL. Subclasses
* only need to override the {@link #getURL(String)}, {@link #extractNegativeResult(URLConnection)}, and perhaps the
* {@link #prepareConnection(URLConnection)} method.
*/
// TODO [FM3] JUnit test (including implementing a HTTP-based template loader to test the new protected methods)
public abstract class URLTemplateLoader implements TemplateLoader {
private static final Logger LOG = LoggerFactory.getLogger(URLTemplateLoader.class);
private Boolean urlConnectionUsesCaches = false;
/**
* Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}.
*/
public Boolean getURLConnectionUsesCaches() {
return urlConnectionUsesCaches;
}
/**
* Sets if {@link URLConnection#setUseCaches(boolean)} will be called, and with what value. By default this is
* {@code false}, because FreeMarker has its own template cache with its own update delay setting
* ({@link Configuration#getTemplateUpdateDelayMilliseconds()}). If this is set to {@code null},
* {@link URLConnection#setUseCaches(boolean)} won't be called.
*/
public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) {
this.urlConnectionUsesCaches = urlConnectionUsesCaches;
}
@Override
public TemplateLoaderSession createSession() {
return null;
}
@Override
public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom,
Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException {
URL url = getURL(name);
if (url == null) {
return TemplateLoadingResult.NOT_FOUND;
}
URLConnection conn = url.openConnection();
Boolean urlConnectionUsesCaches = getURLConnectionUsesCaches();
if (urlConnectionUsesCaches != null) {
conn.setUseCaches(urlConnectionUsesCaches);
}
prepareConnection(conn);
conn.connect();
InputStream inputStream = null;
Long version;
URLTemplateLoadingSource source;
try {
TemplateLoadingResult negativeResult = extractNegativeResult(conn);
if (negativeResult != null) {
return negativeResult;
}
// To prevent clustering issues, getLastModified(fallbackToJarLMD=false)
long lmd = getLastModified(conn, false);
version = lmd != -1 ? lmd : null;
source = new URLTemplateLoadingSource(url);
if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source)
&& Objects.equals(ifVersionDiffersFrom, version)) {
return TemplateLoadingResult.NOT_MODIFIED;
}
inputStream = conn.getInputStream();
} catch (Throwable e) {
try {
if (inputStream == null) {
// There's no URLConnection.close(), so we do this hack. In case of HttpURLConnection we could call
// disconnect(), but that's perhaps too aggressive.
conn.getInputStream().close();
}
} catch (IOException e2) {
LOG.debug("Failed to close connection inputStream", e2);
}
throw e;
}
return new TemplateLoadingResult(source, version, inputStream, null);
}
@Override
public void resetState() {
// Do nothing
}
/**
* {@link URLConnection#getLastModified()} with JDK bug workarounds. Because of JDK-6956385, for files inside a jar,
* it returns the last modification time of the jar itself, rather than the last modification time of the file
* inside the jar.
*
* @param fallbackToJarLMD
* Tells if the file is in side jar, then we should return the last modification time of the jar itself,
* or -1 (to work around JDK-6956385).
*/
public static long getLastModified(URLConnection conn, boolean fallbackToJarLMD) throws IOException {
if (conn instanceof JarURLConnection) {
// There is a bug in sun's jar url connection that causes file handle leaks when calling getLastModified()
// (see https://bugs.openjdk.java.net/browse/JDK-6956385).
// Since the time stamps of jar file contents can't vary independent from the jar file timestamp, just use
// the jar file timestamp
if (fallbackToJarLMD) {
URL jarURL = ((JarURLConnection) conn).getJarFileURL();
if (jarURL.getProtocol().equals("file")) {
// Return the last modified time of the underlying file - saves some opening and closing
return new File(jarURL.getFile()).lastModified();
} else {
// Use the URL mechanism
URLConnection jarConn = null;
try {
jarConn = jarURL.openConnection();
return jarConn.getLastModified();
} finally {
try {
if (jarConn != null) {
jarConn.getInputStream().close();
}
} catch (IOException e) {
LOG.warn("Failed to close URL connection for: {}", conn, e);
}
}
}
} else {
return -1;
}
} else {
return conn.getLastModified();
}
}
/**
* Given a template name (plus potential locale decorations) retrieves an URL that points the template source.
*
* @param name
* the name of the sought template (including the locale decorations, or other decorations the
* {@link TemplateLookupStrategy} uses).
*
* @return An URL that points to the template source, or null if it can be determined that the template source does
* not exist. For many implementations the existence of the template can't be decided at this point, and you
* rely on {@link #extractNegativeResult(URLConnection)} instead.
*/
protected abstract URL getURL(String name);
/**
* Called before the resource if read, checks if we can immediately return a {@link TemplateLoadingResult#NOT_FOUND}
* or {@link TemplateLoadingResult#NOT_MODIFIED}, or throw an {@link IOException}. For example, for a HTTP-based
* storage, the HTTP response status 404 could result in {@link TemplateLoadingResult#NOT_FOUND}, 304 in
* {@link TemplateLoadingResult#NOT_MODIFIED}, and some others, like 500 in throwing an {@link IOException}.
*
* <p>Some
* implementations rely on {@link #getURL(String)} returning {@code null} when a template is missing, in which case
* this method is certainly not applicable.
*/
protected abstract TemplateLoadingResult extractNegativeResult(URLConnection conn) throws IOException;
/**
* Called before anything that causes the connection to actually build up. This is where
* {@link URLConnection#setIfModifiedSince(long)} and such can be called if someone overrides this.
* The default implementation in {@link URLTemplateLoader} does nothing.
*/
protected void prepareConnection(URLConnection conn) {
// Does nothing
}
/**
* Can be used by subclasses to canonicalize URL path prefixes.
* @param prefix the path prefix to canonicalize
* @return the canonicalized prefix. All backslashes are replaced with
* forward slashes, and a trailing slash is appended if the original
* prefix wasn't empty and didn't already end with a slash.
*/
protected static String canonicalizePrefix(String prefix) {
// make it foolproof
prefix = prefix.replace('\\', '/');
// ensure there's a trailing slash
if (prefix.length() > 0 && !prefix.endsWith("/")) {
prefix += "/";
}
return prefix;
}
}