/*
* Copyright 2015 LINE Corporation
*
* LINE Corporation 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 com.linecorp.armeria.server.http.tomcat;
import static java.util.Objects.requireNonNull;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Consumer;
import org.apache.catalina.Realm;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.realm.NullRealm;
import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Builds a {@link TomcatService}. Use the factory methods in {@link TomcatService} if you do not override
* the default settings or you have a configured {@link Tomcat} or {@link Connector} instance.
*/
public final class TomcatServiceBuilder {
private static final Logger logger = LoggerFactory.getLogger(TomcatServiceBuilder.class);
// From Tomcat conf/server.xml
private static final String DEFAULT_SERVICE_NAME = "Catalina";
/**
* Creates a new {@link TomcatServiceBuilder} with the web application at the root directory inside the
* JAR/WAR/directory where the caller class is located at.
*/
public static TomcatServiceBuilder forCurrentClassPath() {
return forCurrentClassPath(2);
}
/**
* Creates a new {@link TomcatServiceBuilder} with the web application at the specified document base
* directory inside the JAR/WAR/directory where the caller class is located at.
*/
public static TomcatServiceBuilder forCurrentClassPath(String docBase) {
return forCurrentClassPath(docBase, 2);
}
static TomcatServiceBuilder forCurrentClassPath(int callDepth) {
return forCurrentClassPath("", callDepth);
}
static TomcatServiceBuilder forCurrentClassPath(String docBase, int callDepth) {
final Class<?> callerClass = TomcatUtil.classContext()[callDepth];
logger.debug("Creating a Tomcat service with the caller class: {}", callerClass.getName());
return forClassPath(callerClass, docBase);
}
/**
* Creates a new {@link TomcatServiceBuilder} with the web application at the root directory inside the
* JAR/WAR/directory where the specified class is located at.
*/
public static TomcatServiceBuilder forClassPath(Class<?> clazz) {
return forClassPath(clazz, "");
}
/**
* Creates a new {@link TomcatServiceBuilder} with the web application at the specified document base
* directory inside the JAR/WAR/directory where the specified class is located at.
*/
public static TomcatServiceBuilder forClassPath(Class<?> clazz, String docBaseOrJarRoot) {
requireNonNull(clazz, "clazz");
requireNonNull(docBaseOrJarRoot, "docBaseOrJarRoot");
final ProtectionDomain pd = clazz.getProtectionDomain();
if (pd == null) {
throw new IllegalArgumentException(clazz + " does not have a protection domain.");
}
final CodeSource cs = pd.getCodeSource();
if (cs == null) {
throw new IllegalArgumentException(clazz + " does not have a code source.");
}
final URL url = cs.getLocation();
if (url == null) {
throw new IllegalArgumentException(clazz + " does not have a location.");
}
if (!"file".equalsIgnoreCase(url.getProtocol())) {
throw new IllegalArgumentException(clazz + " is not on a file system: " + url);
}
File f;
try {
f = new File(url.toURI());
} catch (URISyntaxException ignored) {
f = new File(url.getPath());
}
if (TomcatUtil.isZip(f.toPath())) {
return new TomcatServiceBuilder(f.toPath(), docBaseOrJarRoot);
}
f = fileSystemDocBase(f, docBaseOrJarRoot);
if (!f.isDirectory()) {
throw new IllegalArgumentException(f + " is not a directory.");
}
return forFileSystem(f.toPath());
}
private static File fileSystemDocBase(File rootDir, String relativeDocBase) {
// Append the specified docBase to the root directory to build the actual docBase on file system.
String fileSystemDocBase = rootDir.getPath();
relativeDocBase = relativeDocBase.replace('/', File.separatorChar);
if (fileSystemDocBase.endsWith(File.separator)) {
if (relativeDocBase.startsWith(File.separator)) {
fileSystemDocBase += relativeDocBase.substring(1);
} else {
fileSystemDocBase += relativeDocBase;
}
} else {
if (relativeDocBase.startsWith(File.separator)) {
fileSystemDocBase += relativeDocBase;
} else {
fileSystemDocBase = fileSystemDocBase + File.separatorChar + relativeDocBase;
}
}
return new File(fileSystemDocBase);
}
/**
* Creates a new {@link TomcatServiceBuilder} with the web application at the specified document base,
* which can be a directory or a JAR/WAR file.
*/
public static TomcatServiceBuilder forFileSystem(String docBase) {
return forFileSystem(Paths.get(requireNonNull(docBase, "docBase")));
}
/**
* Creates a new {@link TomcatServiceBuilder} with the web application at the specified document base,
* which can be a directory or a JAR/WAR file.
*/
public static TomcatServiceBuilder forFileSystem(Path docBase) {
return new TomcatServiceBuilder(docBase, null);
}
private final Path docBase;
private final String jarRoot;
private final List<Consumer<? super StandardServer>> configurators = new ArrayList<>();
private String serviceName = DEFAULT_SERVICE_NAME;
private String engineName;
private Path baseDir;
private Realm realm;
private String hostname;
private TomcatServiceBuilder(Path docBase, String jarRoot) {
this.docBase = validateDocBase(docBase);
if (TomcatUtil.isZip(docBase)) {
this.jarRoot = normalizeJarRoot(jarRoot);
} else {
assert jarRoot == null;
this.jarRoot = null;
}
}
private static Path validateDocBase(Path docBase) {
requireNonNull(docBase, "docBase");
docBase = docBase.toAbsolutePath();
if (!Files.exists(docBase)) {
throw new IllegalArgumentException("docBase: " + docBase + " (non-existent)");
}
if (!Files.isDirectory(docBase) && !TomcatUtil.isZip(docBase)) {
throw new IllegalArgumentException(
"docBase: " + docBase + " (expected: a directory or a WAR/JAR file)");
}
return docBase;
}
private static String normalizeJarRoot(String jarRoot) {
if (jarRoot == null || jarRoot.isEmpty() || "/".equals(jarRoot)) {
return "/";
}
if (!jarRoot.startsWith("/")) {
jarRoot = '/' + jarRoot;
}
if (jarRoot.endsWith("/")) {
jarRoot = jarRoot.substring(0, jarRoot.length() - 1);
}
return jarRoot;
}
/**
* Sets the name of the {@link StandardService} of an embedded Tomcat. The default serviceName is
* {@code "Catalina"}.
*/
public TomcatServiceBuilder serviceName(String serviceName) {
this.serviceName = requireNonNull(serviceName, "serviceName");
return this;
}
/**
* Sets the name of the {@link StandardEngine} of an embedded Tomcat. {@link #serviceName(String)} will be
* used instead if not set.
*/
public TomcatServiceBuilder engineName(String engineName) {
this.engineName = requireNonNull(engineName, "engineName");
return this;
}
/**
* Sets the base directory of an embedded Tomcat.
*/
public TomcatServiceBuilder baseDir(String baseDir) {
return baseDir(Paths.get(requireNonNull(baseDir, "baseDir")));
}
/**
* Sets the base directory of an embedded Tomcat.
*/
public TomcatServiceBuilder baseDir(Path baseDir) {
baseDir = requireNonNull(baseDir, "baseDir").toAbsolutePath();
if (!Files.isDirectory(baseDir)) {
throw new IllegalArgumentException("baseDir: " + baseDir + " (expected: a directory)");
}
this.baseDir = baseDir;
return this;
}
/**
* Sets the {@link Realm} of an embedded Tomcat.
*/
public TomcatServiceBuilder realm(Realm realm) {
requireNonNull(realm, "realm");
this.realm = realm;
return this;
}
/**
* Sets the hostname of an embedded Tomcat.
*/
public TomcatServiceBuilder hostname(String hostname) {
this.hostname = validateHostname(hostname);
return this;
}
private static String validateHostname(String hostname) {
requireNonNull(hostname, "hostname");
if (hostname.isEmpty()) {
throw new IllegalArgumentException("hostname is empty.");
}
return hostname;
}
/**
* Adds a {@link Consumer} that performs additional configuration operations against
* the Tomcat {@link StandardServer} created by a {@link TomcatService}.
*/
public TomcatServiceBuilder configurator(Consumer<? super StandardServer> configurator) {
configurators.add(requireNonNull(configurator, "configurator"));
return this;
}
/**
* Creates a new {@link TomcatService}.
*/
public TomcatService build() {
// Create a temporary directory and use it if baseDir is not set.
Path baseDir = this.baseDir;
if (baseDir == null) {
try {
baseDir = Files.createTempDirectory("tomcat-armeria.");
} catch (IOException e) {
throw new TomcatServiceException("failed to create a temporary directory", e);
}
try {
Files.setPosixFilePermissions(baseDir, EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE));
} catch (UnsupportedOperationException ignored) {
// Windows?
} catch (IOException e) {
throw new TomcatServiceException("failed to secure a temporary directory", e);
}
}
// Use a NullRealm if no Realm was specified.
Realm realm = this.realm;
if (realm == null) {
realm = new NullRealm();
}
return TomcatService.forConfig(new TomcatServiceConfig(
serviceName, engineName, baseDir, realm, hostname, docBase, jarRoot,
Collections.unmodifiableList(configurators)));
}
@Override
public String toString() {
return TomcatServiceConfig.toString(
this, serviceName, engineName, baseDir, realm, hostname, docBase, jarRoot);
}
}