/*
* =============================================================================
*
* Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed 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.thymeleaf.templateresource;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import org.thymeleaf.exceptions.TemplateInputException;
import org.thymeleaf.util.StringUtils;
import org.thymeleaf.util.Validate;
/**
* <p>
* Implementation of {@link ITemplateResource} that represents a template accessed through an URL (local or remote).
* </p>
* <p>
* Objects of this class are usually created by {@link org.thymeleaf.templateresolver.UrlTemplateResolver}.
* </p>
*
* @author Daniel Fernández
* @since 3.0.0
*
*/
public final class UrlTemplateResource implements ITemplateResource, Serializable {
private final URL url;
private final String characterEncoding;
public UrlTemplateResource(final String path, final String characterEncoding) throws MalformedURLException {
super();
Validate.notEmpty(path, "Resource Path cannot be null or empty");
// Character encoding CAN be null (system default will be used)
this.url = new URL(path);
this.characterEncoding = characterEncoding;
}
public UrlTemplateResource(final URL url, final String characterEncoding) {
super();
Validate.notNull(url, "Resource URL cannot be null");
// Character encoding CAN be null (system default will be used)
this.url = url;
this.characterEncoding = characterEncoding;
}
public String getDescription() {
return this.url.toString();
}
public String getBaseName() {
return TemplateResourceUtils.computeBaseName(TemplateResourceUtils.cleanPath(this.url.getPath()));
}
public Reader reader() throws IOException {
final InputStream inputStream = inputStream();
if (!StringUtils.isEmptyOrWhitespace(this.characterEncoding)) {
return new BufferedReader(new InputStreamReader(new BufferedInputStream(inputStream), this.characterEncoding));
}
return new BufferedReader(new InputStreamReader(new BufferedInputStream(inputStream)));
}
private InputStream inputStream() throws IOException {
final URLConnection connection = this.url.openConnection();
if (connection.getClass().getSimpleName().startsWith("JNLP")) {
connection.setUseCaches(true);
}
final InputStream inputStream;
try {
inputStream = connection.getInputStream();
} catch (final IOException e) {
if (connection instanceof HttpURLConnection) {
// disconnect() will probably close the underlying socket, which is fine given we had an exception
((HttpURLConnection) connection).disconnect();
}
throw e;
}
return inputStream;
}
public ITemplateResource relative(final String relativeLocation) {
Validate.notEmpty(relativeLocation, "Relative Path cannot be null or empty");
// We will create a new URL using the current one as context, and the relative path as spec
final URL relativeURL;
try {
relativeURL = new URL(this.url, (relativeLocation.charAt(0) == '/' ? relativeLocation.substring(1) : relativeLocation));
} catch (final MalformedURLException e) {
throw new TemplateInputException(
"Could not create relative URL resource for resource \"" + getDescription() + "\" and " +
"relative location \"" + relativeLocation + "\"", e);
}
return new UrlTemplateResource(relativeURL, this.characterEncoding);
}
public boolean exists() {
try {
final String protocol = this.url.getProtocol();
if ("file".equals(protocol)) {
// This is a file system resource, so we will treat it as a file
File file = null;
try {
file = new File(toURI(this.url).getSchemeSpecificPart());
} catch (final URISyntaxException ignored) {
// The URL was not a valid URI (not even after conversion)
file = new File(this.url.getFile());
}
return file.exists();
}
// Not a 'file' URL, so we need to try other less local methods (HTTP/generic connection)
final URLConnection connection = this.url.openConnection();
if (connection.getClass().getSimpleName().startsWith("JNLP")) {
connection.setUseCaches(true);
}
if (connection instanceof HttpURLConnection) {
final HttpURLConnection httpConnection = (HttpURLConnection) connection;
httpConnection.setRequestMethod("HEAD"); // We don't want the document, just know if it exists
int responseCode = httpConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
return true;
} else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
if (httpConnection.getContentLength() >= 0) {
// No status, but at least some content length info!
return true;
}
// At this point, there not much hope, so better even get rid of the socket
httpConnection.disconnect();
return false;
}
// Not an HTTP URL Connection, so let's try direclty obtaining content length info
if (connection.getContentLength() >= 0) {
return true;
}
// Last attempt: open (and then immediately close) the input stream (will raise IOException if not possible)
final InputStream is = inputStream();
is.close();
return true;
} catch (final IOException ignored) {
return false;
}
}
private static URI toURI(final URL url) throws URISyntaxException {
String location = url.toString();
if (location.indexOf(' ') == -1) {
// No need to replace anything
return new URI(location);
}
return new URI(StringUtils.replace(location, " ", "%20"));
}
}