Relocate projects to spring-boot-project
Move projects to better reflect the way that Spring Boot is released. The following projects are under `spring-boot-project`: - `spring-boot` - `spring-boot-autoconfigure` - `spring-boot-tools` - `spring-boot-starters` - `spring-boot-actuator` - `spring-boot-actuator-autoconfigure` - `spring-boot-test` - `spring-boot-test-autoconfigure` - `spring-boot-devtools` - `spring-boot-cli` - `spring-boot-docs` See gh-9316
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* A {@code BuildPropertiesWriter} writes the {@code build-info.properties} for
|
||||
* consumption by the Actuator.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public final class BuildPropertiesWriter {
|
||||
|
||||
private final File outputFile;
|
||||
|
||||
/**
|
||||
* Creates a new {@code BuildPropertiesWriter} that will write to the given
|
||||
* {@code outputFile}.
|
||||
* @param outputFile the output file
|
||||
*/
|
||||
public BuildPropertiesWriter(File outputFile) {
|
||||
this.outputFile = outputFile;
|
||||
}
|
||||
|
||||
public void writeBuildProperties(ProjectDetails projectDetails) throws IOException {
|
||||
Properties properties = createBuildInfo(projectDetails);
|
||||
createFileIfNecessary(this.outputFile);
|
||||
try (FileOutputStream outputStream = new FileOutputStream(this.outputFile)) {
|
||||
properties.store(outputStream, "Properties");
|
||||
}
|
||||
}
|
||||
|
||||
private void createFileIfNecessary(File file) throws IOException {
|
||||
if (file.exists()) {
|
||||
return;
|
||||
}
|
||||
File parent = file.getParentFile();
|
||||
if (!parent.isDirectory() && !parent.mkdirs()) {
|
||||
throw new IllegalStateException("Cannot create parent directory for '"
|
||||
+ this.outputFile.getAbsolutePath() + "'");
|
||||
}
|
||||
if (!file.createNewFile()) {
|
||||
throw new IllegalStateException("Cannot create target file '"
|
||||
+ this.outputFile.getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
protected Properties createBuildInfo(ProjectDetails project) {
|
||||
Properties properties = new Properties();
|
||||
properties.put("build.group", project.getGroup());
|
||||
properties.put("build.artifact", project.getArtifact());
|
||||
properties.put("build.name", project.getName());
|
||||
properties.put("build.version", project.getVersion());
|
||||
properties.put("build.time", formatDate(new Date()));
|
||||
if (project.getAdditionalProperties() != null) {
|
||||
for (Map.Entry<String, String> entry : project.getAdditionalProperties()
|
||||
.entrySet()) {
|
||||
properties.put("build." + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private String formatDate(Date date) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
return sdf.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build-system agnostic details of a project.
|
||||
*/
|
||||
public static final class ProjectDetails {
|
||||
|
||||
private final String group;
|
||||
|
||||
private final String artifact;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String version;
|
||||
|
||||
private final Map<String, String> additionalProperties;
|
||||
|
||||
public ProjectDetails(String group, String artifact, String version, String name,
|
||||
Map<String, String> additionalProperties) {
|
||||
this.group = group;
|
||||
this.artifact = artifact;
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
validateAdditionalProperties(additionalProperties);
|
||||
this.additionalProperties = additionalProperties;
|
||||
}
|
||||
|
||||
private static void validateAdditionalProperties(
|
||||
Map<String, String> additionalProperties) {
|
||||
if (additionalProperties != null) {
|
||||
for (Entry<String, String> property : additionalProperties.entrySet()) {
|
||||
if (property.getValue() == null) {
|
||||
throw new NullAdditionalPropertyValueException(property.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
public String getArtifact() {
|
||||
return this.artifact;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public Map<String, String> getAdditionalProperties() {
|
||||
return this.additionalProperties;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when an additional property with a null value is encountered.
|
||||
*/
|
||||
public static class NullAdditionalPropertyValueException
|
||||
extends IllegalArgumentException {
|
||||
|
||||
public NullAdditionalPropertyValueException(String name) {
|
||||
super("Additional property '" + name + "' is illegal as its value is null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Additional interface that can be implemented by {@link Layout Layouts} that write their
|
||||
* own loader classes.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CustomLoaderLayout {
|
||||
|
||||
/**
|
||||
* Write the required loader classes into the JAR.
|
||||
* @param writer the writer used to write the classes
|
||||
* @throws IOException if the classes cannot be written
|
||||
*/
|
||||
void writeLoadedClasses(LoaderClassesWriter writer) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link LaunchScript}. Provides the default Spring Boot launch
|
||||
* script or can load a specific script File. Also support mustache style template
|
||||
* expansion of the form <code>{{name:default}}</code>.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Justin Rosenberg
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class DefaultLaunchScript implements LaunchScript {
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern
|
||||
.compile("\\{\\{(\\w+)(:.*?)?\\}\\}(?!\\})");
|
||||
|
||||
private static final Set<String> FILE_PATH_KEYS = Collections
|
||||
.unmodifiableSet(new HashSet<>(Arrays.asList("inlinedConfScript")));
|
||||
|
||||
private final String content;
|
||||
|
||||
/**
|
||||
* Create a new {@link DefaultLaunchScript} instance.
|
||||
* @param file the source script file or {@code null} to use the default
|
||||
* @param properties an optional set of script properties used for variable expansion
|
||||
* @throws IOException if the script cannot be loaded
|
||||
*/
|
||||
public DefaultLaunchScript(File file, Map<?, ?> properties) throws IOException {
|
||||
String content = loadContent(file);
|
||||
this.content = expandPlaceholders(content, properties);
|
||||
}
|
||||
|
||||
private String loadContent(File file) throws IOException {
|
||||
if (file == null) {
|
||||
return loadContent(getClass().getResourceAsStream("launch.script"));
|
||||
}
|
||||
return loadContent(new FileInputStream(file));
|
||||
}
|
||||
|
||||
private String loadContent(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
copy(inputStream, outputStream);
|
||||
return new String(outputStream.toByteArray(), UTF_8);
|
||||
}
|
||||
finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void copy(InputStream inputStream, OutputStream outputStream)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
private String expandPlaceholders(String content, Map<?, ?> properties)
|
||||
throws IOException {
|
||||
StringBuffer expanded = new StringBuffer();
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(content);
|
||||
while (matcher.find()) {
|
||||
String name = matcher.group(1);
|
||||
final String value;
|
||||
String defaultValue = matcher.group(2);
|
||||
if (properties != null && properties.containsKey(name)) {
|
||||
Object propertyValue = properties.get(name);
|
||||
if (FILE_PATH_KEYS.contains(name)) {
|
||||
value = parseFilePropertyValue(properties.get(name));
|
||||
}
|
||||
else {
|
||||
value = propertyValue.toString();
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = (defaultValue == null ? matcher.group(0)
|
||||
: defaultValue.substring(1));
|
||||
}
|
||||
matcher.appendReplacement(expanded, value.replace("$", "\\$"));
|
||||
}
|
||||
matcher.appendTail(expanded);
|
||||
return expanded.toString();
|
||||
}
|
||||
|
||||
private String parseFilePropertyValue(Object propertyValue) throws IOException {
|
||||
if (propertyValue instanceof File) {
|
||||
return loadContent((File) propertyValue);
|
||||
}
|
||||
return loadContent(new File(propertyValue.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
return this.content.getBytes(UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link LayoutFactory}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.5.0
|
||||
*/
|
||||
public class DefaultLayoutFactory implements LayoutFactory {
|
||||
|
||||
@Override
|
||||
public Layout getLayout(File source) {
|
||||
return Layouts.forFile(source);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Utilities for manipulating files and directories in Spring Boot tooling.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public abstract class FileUtils {
|
||||
|
||||
/**
|
||||
* Utility to remove duplicate files from an "output" directory if they already exist
|
||||
* in an "origin". Recursively scans the origin directory looking for files (not
|
||||
* directories) that exist in both places and deleting the copy.
|
||||
* @param outputDirectory the output directory
|
||||
* @param originDirectory the origin directory
|
||||
*/
|
||||
public static void removeDuplicatesFromOutputDirectory(File outputDirectory,
|
||||
File originDirectory) {
|
||||
if (originDirectory.isDirectory()) {
|
||||
for (String name : originDirectory.list()) {
|
||||
File targetFile = new File(outputDirectory, name);
|
||||
if (targetFile.exists() && targetFile.canWrite()) {
|
||||
if (!targetFile.isDirectory()) {
|
||||
targetFile.delete();
|
||||
}
|
||||
else {
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(targetFile,
|
||||
new File(originDirectory, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a SHA.1 Hash for a given file.
|
||||
* @param file the file to hash
|
||||
* @return the hash value as a String
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
public static String sha1Hash(File file) throws IOException {
|
||||
try {
|
||||
try (DigestInputStream inputStream = new DigestInputStream(
|
||||
new FileInputStream(file), MessageDigest.getInstance("SHA-1"))) {
|
||||
byte[] buffer = new byte[4098];
|
||||
while (inputStream.read(buffer) != -1) {
|
||||
// Read the entire stream
|
||||
}
|
||||
return bytesToHex(inputStream.getMessageDigest().digest());
|
||||
}
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.zip.UnixStat;
|
||||
|
||||
/**
|
||||
* Writes JAR content, ensuring valid directory entries are always create and duplicate
|
||||
* items are ignored.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
|
||||
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
|
||||
|
||||
private static final int BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
private final JarArchiveOutputStream jarOutput;
|
||||
|
||||
private final Set<String> writtenEntries = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Create a new {@link JarWriter} instance.
|
||||
* @param file the file to write
|
||||
* @throws IOException if the file cannot be opened
|
||||
* @throws FileNotFoundException if the file cannot be found
|
||||
*/
|
||||
public JarWriter(File file) throws FileNotFoundException, IOException {
|
||||
this(file, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JarWriter} instance.
|
||||
* @param file the file to write
|
||||
* @param launchScript an optional launch script to prepend to the front of the jar
|
||||
* @throws IOException if the file cannot be opened
|
||||
* @throws FileNotFoundException if the file cannot be found
|
||||
*/
|
||||
public JarWriter(File file, LaunchScript launchScript)
|
||||
throws FileNotFoundException, IOException {
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||
if (launchScript != null) {
|
||||
fileOutputStream.write(launchScript.toByteArray());
|
||||
setExecutableFilePermission(file);
|
||||
}
|
||||
this.jarOutput = new JarArchiveOutputStream(fileOutputStream);
|
||||
this.jarOutput.setEncoding("UTF-8");
|
||||
}
|
||||
|
||||
private void setExecutableFilePermission(File file) {
|
||||
try {
|
||||
Path path = file.toPath();
|
||||
Set<PosixFilePermission> permissions = new HashSet<>(
|
||||
Files.getPosixFilePermissions(path));
|
||||
permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
Files.setPosixFilePermissions(path, permissions);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// Ignore and continue creating the jar
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the specified manifest.
|
||||
* @param manifest the manifest to write
|
||||
* @throws IOException of the manifest cannot be written
|
||||
*/
|
||||
public void writeManifest(final Manifest manifest) throws IOException {
|
||||
JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");
|
||||
writeEntry(entry, manifest::write);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all entries from the specified jar file.
|
||||
* @param jarFile the source jar file
|
||||
* @throws IOException if the entries cannot be written
|
||||
*/
|
||||
public void writeEntries(JarFile jarFile) throws IOException {
|
||||
this.writeEntries(jarFile, new IdentityEntryTransformer());
|
||||
}
|
||||
|
||||
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer)
|
||||
throws IOException {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
|
||||
setUpStoredEntryIfNecessary(jarFile, entry);
|
||||
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(
|
||||
jarFile.getInputStream(entry))) {
|
||||
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true);
|
||||
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
|
||||
if (transformedEntry != null) {
|
||||
writeEntry(transformedEntry, entryWriter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setUpStoredEntryIfNecessary(JarFile jarFile, JarArchiveEntry entry)
|
||||
throws IOException {
|
||||
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(
|
||||
jarFile.getInputStream(entry))) {
|
||||
if (inputStream.hasZipHeader() && entry.getMethod() != ZipEntry.STORED) {
|
||||
new CrcAndSize(inputStream).setupStoredEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an entry. The {@code inputStream} is closed once the entry has been written
|
||||
* @param entryName The name of the entry
|
||||
* @param inputStream The stream from which the entry's data can be read
|
||||
* @throws IOException if the write fails
|
||||
*/
|
||||
@Override
|
||||
public void writeEntry(String entryName, InputStream inputStream) throws IOException {
|
||||
JarArchiveEntry entry = new JarArchiveEntry(entryName);
|
||||
writeEntry(entry, new InputStreamEntryWriter(inputStream, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a nested library.
|
||||
* @param destination the destination of the library
|
||||
* @param library the library
|
||||
* @throws IOException if the write fails
|
||||
*/
|
||||
public void writeNestedLibrary(String destination, Library library)
|
||||
throws IOException {
|
||||
File file = library.getFile();
|
||||
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName());
|
||||
entry.setTime(getNestedLibraryTime(file));
|
||||
if (library.isUnpackRequired()) {
|
||||
entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
|
||||
}
|
||||
new CrcAndSize(file).setupStoredEntry(entry);
|
||||
writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
|
||||
}
|
||||
|
||||
private long getNestedLibraryTime(File file) {
|
||||
try {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
if (!entry.isDirectory()) {
|
||||
return entry.getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore and just use the source file timestamp
|
||||
}
|
||||
return file.lastModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the required spring-boot-loader classes to the JAR.
|
||||
* @throws IOException if the classes cannot be written
|
||||
*/
|
||||
@Override
|
||||
public void writeLoaderClasses() throws IOException {
|
||||
writeLoaderClasses(NESTED_LOADER_JAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the required spring-boot-loader classes to the JAR.
|
||||
* @param loaderJarResourceName the name of the resource containing the loader classes
|
||||
* to be written
|
||||
* @throws IOException if the classes cannot be written
|
||||
*/
|
||||
@Override
|
||||
public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
|
||||
URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
|
||||
JarInputStream inputStream = new JarInputStream(
|
||||
new BufferedInputStream(loaderJar.openStream()));
|
||||
JarEntry entry;
|
||||
while ((entry = inputStream.getNextJarEntry()) != null) {
|
||||
if (entry.getName().endsWith(".class")) {
|
||||
writeEntry(new JarArchiveEntry(entry),
|
||||
new InputStreamEntryWriter(inputStream, false));
|
||||
}
|
||||
}
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the writer.
|
||||
* @throws IOException if the file cannot be closed
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.jarOutput.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual write of a {@link JarEntry}. All other {@code write} method
|
||||
* delegate to this one.
|
||||
* @param entry the entry to write
|
||||
* @param entryWriter the entry writer or {@code null} if there is no content
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter)
|
||||
throws IOException {
|
||||
String parent = entry.getName();
|
||||
if (parent.endsWith("/")) {
|
||||
parent = parent.substring(0, parent.length() - 1);
|
||||
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
|
||||
}
|
||||
else {
|
||||
entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
|
||||
}
|
||||
if (parent.lastIndexOf("/") != -1) {
|
||||
parent = parent.substring(0, parent.lastIndexOf("/") + 1);
|
||||
if (!parent.isEmpty()) {
|
||||
writeEntry(new JarArchiveEntry(parent), null);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.writtenEntries.add(entry.getName())) {
|
||||
this.jarOutput.putArchiveEntry(entry);
|
||||
if (entryWriter != null) {
|
||||
entryWriter.write(this.jarOutput);
|
||||
}
|
||||
this.jarOutput.closeArchiveEntry();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to write jar entry date.
|
||||
*/
|
||||
private interface EntryWriter {
|
||||
|
||||
/**
|
||||
* Write entry data to the specified output stream.
|
||||
* @param outputStream the destination for the data
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
void write(OutputStream outputStream) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link EntryWriter} that writes content from an {@link InputStream}.
|
||||
*/
|
||||
private static class InputStreamEntryWriter implements EntryWriter {
|
||||
|
||||
private final InputStream inputStream;
|
||||
|
||||
private final boolean close;
|
||||
|
||||
InputStreamEntryWriter(InputStream inputStream, boolean close) {
|
||||
this.inputStream = inputStream;
|
||||
this.close = close;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(OutputStream outputStream) throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = this.inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
if (this.close) {
|
||||
this.inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link InputStream} that can peek ahead at zip header bytes.
|
||||
*/
|
||||
private static class ZipHeaderPeekInputStream extends FilterInputStream {
|
||||
|
||||
private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 };
|
||||
|
||||
private final byte[] header;
|
||||
|
||||
private ByteArrayInputStream headerStream;
|
||||
|
||||
protected ZipHeaderPeekInputStream(InputStream in) throws IOException {
|
||||
super(in);
|
||||
this.header = new byte[4];
|
||||
int len = in.read(this.header);
|
||||
this.headerStream = new ByteArrayInputStream(this.header, 0, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int read = (this.headerStream == null ? -1 : this.headerStream.read());
|
||||
if (read != -1) {
|
||||
this.headerStream = null;
|
||||
return read;
|
||||
}
|
||||
return super.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int read = (this.headerStream == null ? -1
|
||||
: this.headerStream.read(b, off, len));
|
||||
if (read != -1) {
|
||||
this.headerStream = null;
|
||||
return read;
|
||||
}
|
||||
return super.read(b, off, len);
|
||||
}
|
||||
|
||||
public boolean hasZipHeader() {
|
||||
return Arrays.equals(this.header, ZIP_HEADER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data holder for CRC and Size.
|
||||
*/
|
||||
private static class CrcAndSize {
|
||||
|
||||
private final CRC32 crc = new CRC32();
|
||||
|
||||
private long size;
|
||||
|
||||
CrcAndSize(File file) throws IOException {
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
load(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
CrcAndSize(InputStream inputStream) throws IOException {
|
||||
load(inputStream);
|
||||
}
|
||||
|
||||
private void load(InputStream inputStream) throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
this.crc.update(buffer, 0, bytesRead);
|
||||
this.size += bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
public void setupStoredEntry(JarArchiveEntry entry) {
|
||||
entry.setSize(this.size);
|
||||
entry.setCompressedSize(this.size);
|
||||
entry.setCrc(this.crc.getValue());
|
||||
entry.setMethod(ZipEntry.STORED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
|
||||
* entries} during the writing process.
|
||||
*/
|
||||
interface EntryTransformer {
|
||||
|
||||
JarArchiveEntry transform(JarArchiveEntry jarEntry);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EntryTransformer} that returns the entry unchanged.
|
||||
*/
|
||||
private static final class IdentityEntryTransformer implements EntryTransformer {
|
||||
|
||||
@Override
|
||||
public JarArchiveEntry transform(JarArchiveEntry jarEntry) {
|
||||
return jarEntry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Provides access to the java binary executable, regardless of OS.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class JavaExecutable {
|
||||
|
||||
private File file;
|
||||
|
||||
public JavaExecutable() {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
Assert.state(StringUtils.hasLength(javaHome),
|
||||
"Unable to find java executable due to missing 'java.home'");
|
||||
this.file = findInJavaHome(javaHome);
|
||||
}
|
||||
|
||||
private File findInJavaHome(String javaHome) {
|
||||
File bin = new File(new File(javaHome), "bin");
|
||||
File command = new File(bin, "java.exe");
|
||||
command = (command.exists() ? command : new File(bin, "java"));
|
||||
Assert.state(command.exists(), "Unable to find java in " + javaHome);
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ProcessBuilder} that will run with the Java executable.
|
||||
* @param arguments the command arguments
|
||||
* @return a {@link ProcessBuilder}
|
||||
*/
|
||||
public ProcessBuilder processBuilder(String... arguments) {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(toString());
|
||||
processBuilder.command().addAll(Arrays.asList(arguments));
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return this.file.getCanonicalPath();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
/**
|
||||
* A script that can be prepended to the front of a JAR file to make it executable.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LaunchScript {
|
||||
|
||||
/**
|
||||
* The content of the launch script as a byte array.
|
||||
* @return the script bytes
|
||||
*/
|
||||
byte[] toByteArray();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
/**
|
||||
* Strategy interface used to determine the layout for a particular type of archive.
|
||||
* Layouts may additionally implement {@link CustomLoaderLayout} if they wish to write
|
||||
* custom loader classes.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see Layouts
|
||||
* @see RepackagingLayout
|
||||
* @see CustomLoaderLayout
|
||||
*/
|
||||
public interface Layout {
|
||||
|
||||
/**
|
||||
* Returns the launcher class name for this layout.
|
||||
* @return the launcher class name
|
||||
*/
|
||||
String getLauncherClassName();
|
||||
|
||||
/**
|
||||
* Returns the destination path for a given library.
|
||||
* @param libraryName the name of the library (excluding any path)
|
||||
* @param scope the scope of the library
|
||||
* @return the destination relative to the root of the archive (should end with '/')
|
||||
* or {@code null} if the library should not be included.
|
||||
*/
|
||||
String getLibraryDestination(String libraryName, LibraryScope scope);
|
||||
|
||||
/**
|
||||
* Returns the location of classes within the archive.
|
||||
* @return the classes location
|
||||
*/
|
||||
String getClassesLocation();
|
||||
|
||||
/**
|
||||
* Returns if loader classes should be included to make the archive executable.
|
||||
* @return if the layout is executable
|
||||
*/
|
||||
boolean isExecutable();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Factory interface used to create a {@link Layout}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LayoutFactory {
|
||||
|
||||
/**
|
||||
* Return a {@link Layout} for the specified source file.
|
||||
* @param source the source file
|
||||
* @return the layout to use for the file
|
||||
*/
|
||||
Layout getLayout(File source);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Common {@link Layout}s.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public final class Layouts {
|
||||
|
||||
private Layouts() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a layout for the given source file.
|
||||
* @param file the source file
|
||||
* @return a {@link Layout}
|
||||
*/
|
||||
public static Layout forFile(File file) {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("File must not be null");
|
||||
}
|
||||
if (file.getName().toLowerCase().endsWith(".jar")) {
|
||||
return new Jar();
|
||||
}
|
||||
if (file.getName().toLowerCase().endsWith(".war")) {
|
||||
return new War();
|
||||
}
|
||||
if (file.isDirectory() || file.getName().toLowerCase().endsWith(".zip")) {
|
||||
return new Expanded();
|
||||
}
|
||||
throw new IllegalStateException("Unable to deduce layout for '" + file + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executable JAR layout.
|
||||
*/
|
||||
public static class Jar implements RepackagingLayout {
|
||||
|
||||
@Override
|
||||
public String getLauncherClassName() {
|
||||
return "org.springframework.boot.loader.JarLauncher";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLibraryDestination(String libraryName, LibraryScope scope) {
|
||||
return "BOOT-INF/lib/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClassesLocation() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepackagedClassesLocation() {
|
||||
return "BOOT-INF/classes/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecutable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executable expanded archive layout.
|
||||
*/
|
||||
public static class Expanded extends Jar {
|
||||
|
||||
@Override
|
||||
public String getLauncherClassName() {
|
||||
return "org.springframework.boot.loader.PropertiesLauncher";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* No layout.
|
||||
*/
|
||||
public static class None extends Jar {
|
||||
|
||||
@Override
|
||||
public String getLauncherClassName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecutable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executable WAR layout.
|
||||
*/
|
||||
public static class War implements Layout {
|
||||
|
||||
private static final Map<LibraryScope, String> scopeDestinations;
|
||||
|
||||
static {
|
||||
Map<LibraryScope, String> map = new HashMap<>();
|
||||
map.put(LibraryScope.COMPILE, "WEB-INF/lib/");
|
||||
map.put(LibraryScope.CUSTOM, "WEB-INF/lib/");
|
||||
map.put(LibraryScope.RUNTIME, "WEB-INF/lib/");
|
||||
map.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/");
|
||||
scopeDestinations = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLauncherClassName() {
|
||||
return "org.springframework.boot.loader.WarLauncher";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLibraryDestination(String libraryName, LibraryScope scope) {
|
||||
return scopeDestinations.get(scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClassesLocation() {
|
||||
return "WEB-INF/classes/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecutable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Encapsulates information about libraries that may be packed into the archive.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Libraries {
|
||||
|
||||
/**
|
||||
* Represents no libraries.
|
||||
*/
|
||||
Libraries NONE = (callback) -> {
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterate all relevant libraries.
|
||||
* @param callback a callback for each relevant library.
|
||||
* @throws IOException if the operation fails
|
||||
*/
|
||||
void doWithLibraries(LibraryCallback callback) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Encapsulates information about a single library that may be packed into the archive.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.1.2
|
||||
* @see Libraries
|
||||
*/
|
||||
public class Library {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final File file;
|
||||
|
||||
private final LibraryScope scope;
|
||||
|
||||
private final boolean unpackRequired;
|
||||
|
||||
/**
|
||||
* Create a new {@link Library}.
|
||||
* @param file the source file
|
||||
* @param scope the scope of the library
|
||||
*/
|
||||
public Library(File file, LibraryScope scope) {
|
||||
this(file, scope, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Library}.
|
||||
* @param file the source file
|
||||
* @param scope the scope of the library
|
||||
* @param unpackRequired if the library needs to be unpacked before it can be used
|
||||
*/
|
||||
public Library(File file, LibraryScope scope, boolean unpackRequired) {
|
||||
this(null, file, scope, unpackRequired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Library}.
|
||||
* @param name the name of the library as it should be written or {@code null} to use
|
||||
* the file name
|
||||
* @param file the source file
|
||||
* @param scope the scope of the library
|
||||
* @param unpackRequired if the library needs to be unpacked before it can be used
|
||||
*/
|
||||
public Library(String name, File file, LibraryScope scope, boolean unpackRequired) {
|
||||
this.name = (name == null ? file.getName() : name);
|
||||
this.file = file;
|
||||
this.scope = scope;
|
||||
this.unpackRequired = unpackRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of file as it should be written.
|
||||
* @return then name.
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the library file.
|
||||
* @return the file
|
||||
*/
|
||||
public File getFile() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the scope of the library.
|
||||
* @return the scope
|
||||
*/
|
||||
public LibraryScope getScope() {
|
||||
return this.scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the file cannot be used directly as a nested jar and needs to be
|
||||
* unpacked.
|
||||
* @return if unpack is required
|
||||
*/
|
||||
public boolean isUnpackRequired() {
|
||||
return this.unpackRequired;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Callback interface used to iterate {@link Libraries}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LibraryCallback {
|
||||
|
||||
/**
|
||||
* Callback for a single library backed by a {@link File}.
|
||||
* @param library the library
|
||||
* @throws IOException if the operation fails
|
||||
*/
|
||||
void library(Library library) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
/**
|
||||
* The scope of a library. The common {@link #COMPILE}, {@link #RUNTIME} and
|
||||
* {@link #PROVIDED} scopes are defined here and supported by the common {@link Layouts}.
|
||||
* A custom {@link Layout} can handle additional scopes as required.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public interface LibraryScope {
|
||||
|
||||
@Override
|
||||
String toString();
|
||||
|
||||
/**
|
||||
* The library is used at compile time and runtime.
|
||||
*/
|
||||
LibraryScope COMPILE = new LibraryScope() {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "compile";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The library is used at runtime but not needed for compile.
|
||||
*/
|
||||
LibraryScope RUNTIME = new LibraryScope() {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "runtime";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The library is needed for compile but is usually provided when running.
|
||||
*/
|
||||
LibraryScope PROVIDED = new LibraryScope() {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "provided";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Marker for custom scope when custom configuration is used.
|
||||
*/
|
||||
LibraryScope CUSTOM = new LibraryScope() {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "custom";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Writer used by {@link CustomLoaderLayout CustomLoaderLayouts} to write classes into a
|
||||
* repackaged JAR.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.5.0
|
||||
*/
|
||||
public interface LoaderClassesWriter {
|
||||
|
||||
/**
|
||||
* Write the default required spring-boot-loader classes to the JAR.
|
||||
* @throws IOException if the classes cannot be written
|
||||
*/
|
||||
void writeLoaderClasses() throws IOException;
|
||||
|
||||
/**
|
||||
* Write custom required spring-boot-loader classes to the JAR.
|
||||
* @param loaderJarResourceName the name of the resource containing the loader classes
|
||||
* to be written
|
||||
* @throws IOException if the classes cannot be written
|
||||
*/
|
||||
void writeLoaderClasses(String loaderJarResourceName) throws IOException;
|
||||
|
||||
/**
|
||||
* Write a single entry to the JAR.
|
||||
* @param name the name of the entry
|
||||
* @param inputStream the input stream content
|
||||
* @throws IOException if the entry cannot be written
|
||||
*/
|
||||
void writeEntry(String name, InputStream inputStream) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import org.slf4j.ILoggerFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.impl.StaticLoggerBinder;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Utility to initialize logback (when present) to use INFO level logging.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public abstract class LogbackInitializer {
|
||||
|
||||
public static void initialize() {
|
||||
if (ClassUtils.isPresent("org.slf4j.impl.StaticLoggerBinder", null)
|
||||
&& ClassUtils.isPresent("ch.qos.logback.classic.Logger", null)) {
|
||||
new Initializer().setRootLogLevel();
|
||||
}
|
||||
}
|
||||
|
||||
private static class Initializer {
|
||||
|
||||
public void setRootLogLevel() {
|
||||
ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
|
||||
Logger logger = factory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
((ch.qos.logback.classic.Logger) logger).setLevel(Level.INFO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.springframework.asm.AnnotationVisitor;
|
||||
import org.springframework.asm.ClassReader;
|
||||
import org.springframework.asm.ClassVisitor;
|
||||
import org.springframework.asm.MethodVisitor;
|
||||
import org.springframework.asm.Opcodes;
|
||||
import org.springframework.asm.Type;
|
||||
|
||||
/**
|
||||
* Finds any class with a {@code public static main} method by performing a breadth first
|
||||
* search.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class MainClassFinder {
|
||||
|
||||
private static final String DOT_CLASS = ".class";
|
||||
|
||||
private static final Type STRING_ARRAY_TYPE = Type.getType(String[].class);
|
||||
|
||||
private static final Type MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE,
|
||||
STRING_ARRAY_TYPE);
|
||||
|
||||
private static final String MAIN_METHOD_NAME = "main";
|
||||
|
||||
private static final FileFilter CLASS_FILE_FILTER = MainClassFinder::isClassFile;
|
||||
|
||||
private static final FileFilter PACKAGE_FOLDER_FILTER = MainClassFinder::isPackageFolder;
|
||||
|
||||
private static boolean isClassFile(File file) {
|
||||
return file.isFile() && file.getName().endsWith(DOT_CLASS);
|
||||
}
|
||||
|
||||
private static boolean isPackageFolder(File file) {
|
||||
return file.isDirectory() && !file.getName().startsWith(".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the main class from a given folder.
|
||||
* @param rootFolder the root folder to search
|
||||
* @return the main class or {@code null}
|
||||
* @throws IOException if the folder cannot be read
|
||||
*/
|
||||
public static String findMainClass(File rootFolder) throws IOException {
|
||||
return doWithMainClasses(rootFolder, MainClass::getName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single main class from the given {@code rootFolder}.
|
||||
* @param rootFolder the root folder to search
|
||||
* @return the main class or {@code null}
|
||||
* @throws IOException if the folder cannot be read
|
||||
*/
|
||||
public static String findSingleMainClass(File rootFolder) throws IOException {
|
||||
return findSingleMainClass(rootFolder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single main class from the given {@code rootFolder}. A main class annotated
|
||||
* with an annotation with the given {@code annotationName} will be preferred over a
|
||||
* main class with no such annotation.
|
||||
* @param rootFolder the root folder to search
|
||||
* @param annotationName the name of the annotation that may be present on the main
|
||||
* class
|
||||
* @return the main class or {@code null}
|
||||
* @throws IOException if the folder cannot be read
|
||||
*/
|
||||
public static String findSingleMainClass(File rootFolder, String annotationName)
|
||||
throws IOException {
|
||||
SingleMainClassCallback callback = new SingleMainClassCallback(annotationName);
|
||||
MainClassFinder.doWithMainClasses(rootFolder, callback);
|
||||
return callback.getMainClassName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given callback operation on all main classes from the given root
|
||||
* folder.
|
||||
* @param <T> the result type
|
||||
* @param rootFolder the root folder
|
||||
* @param callback the callback
|
||||
* @return the first callback result or {@code null}
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
static <T> T doWithMainClasses(File rootFolder, MainClassCallback<T> callback)
|
||||
throws IOException {
|
||||
if (!rootFolder.exists()) {
|
||||
return null; // nothing to do
|
||||
}
|
||||
if (!rootFolder.isDirectory()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid root folder '" + rootFolder + "'");
|
||||
}
|
||||
String prefix = rootFolder.getAbsolutePath() + "/";
|
||||
Deque<File> stack = new ArrayDeque<>();
|
||||
stack.push(rootFolder);
|
||||
while (!stack.isEmpty()) {
|
||||
File file = stack.pop();
|
||||
if (file.isFile()) {
|
||||
try (InputStream inputStream = new FileInputStream(file)) {
|
||||
ClassDescriptor classDescriptor = createClassDescriptor(inputStream);
|
||||
if (classDescriptor != null && classDescriptor.isMainMethodFound()) {
|
||||
String className = convertToClassName(file.getAbsolutePath(),
|
||||
prefix);
|
||||
T result = callback.doWith(new MainClass(className,
|
||||
classDescriptor.getAnnotationNames()));
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
pushAllSorted(stack, file.listFiles(PACKAGE_FOLDER_FILTER));
|
||||
pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void pushAllSorted(Deque<File> stack, File[] files) {
|
||||
Arrays.sort(files, Comparator.comparing(File::getName));
|
||||
for (File file : files) {
|
||||
stack.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the main class in a given jar file.
|
||||
* @param jarFile the jar file to search
|
||||
* @param classesLocation the location within the jar containing classes
|
||||
* @return the main class or {@code null}
|
||||
* @throws IOException if the jar file cannot be read
|
||||
*/
|
||||
public static String findMainClass(JarFile jarFile, String classesLocation)
|
||||
throws IOException {
|
||||
return doWithMainClasses(jarFile, classesLocation, MainClass::getName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single main class in a given jar file.
|
||||
* @param jarFile the jar file to search
|
||||
* @param classesLocation the location within the jar containing classes
|
||||
* @return the main class or {@code null}
|
||||
* @throws IOException if the jar file cannot be read
|
||||
*/
|
||||
public static String findSingleMainClass(JarFile jarFile, String classesLocation)
|
||||
throws IOException {
|
||||
return findSingleMainClass(jarFile, classesLocation, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single main class in a given jar file. A main class annotated with an
|
||||
* annotation with the given {@code annotationName} will be preferred over a main
|
||||
* class with no such annotation.
|
||||
* @param jarFile the jar file to search
|
||||
* @param classesLocation the location within the jar containing classes
|
||||
* @param annotationName the name of the annotation that may be present on the main
|
||||
* class
|
||||
* @return the main class or {@code null}
|
||||
* @throws IOException if the jar file cannot be read
|
||||
*/
|
||||
public static String findSingleMainClass(JarFile jarFile, String classesLocation,
|
||||
String annotationName) throws IOException {
|
||||
SingleMainClassCallback callback = new SingleMainClassCallback(annotationName);
|
||||
MainClassFinder.doWithMainClasses(jarFile, classesLocation, callback);
|
||||
return callback.getMainClassName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given callback operation on all main classes from the given jar.
|
||||
* @param <T> the result type
|
||||
* @param jarFile the jar file to search
|
||||
* @param classesLocation the location within the jar containing classes
|
||||
* @param callback the callback
|
||||
* @return the first callback result or {@code null}
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
static <T> T doWithMainClasses(JarFile jarFile, String classesLocation,
|
||||
MainClassCallback<T> callback) throws IOException {
|
||||
List<JarEntry> classEntries = getClassEntries(jarFile, classesLocation);
|
||||
classEntries.sort(new ClassEntryComparator());
|
||||
for (JarEntry entry : classEntries) {
|
||||
try (InputStream inputStream = new BufferedInputStream(
|
||||
jarFile.getInputStream(entry))) {
|
||||
ClassDescriptor classDescriptor = createClassDescriptor(inputStream);
|
||||
if (classDescriptor != null && classDescriptor.isMainMethodFound()) {
|
||||
String className = convertToClassName(entry.getName(),
|
||||
classesLocation);
|
||||
T result = callback.doWith(new MainClass(className,
|
||||
classDescriptor.getAnnotationNames()));
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String convertToClassName(String name, String prefix) {
|
||||
name = name.replace('/', '.');
|
||||
name = name.replace('\\', '.');
|
||||
name = name.substring(0, name.length() - DOT_CLASS.length());
|
||||
if (prefix != null) {
|
||||
name = name.substring(prefix.length());
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static List<JarEntry> getClassEntries(JarFile source,
|
||||
String classesLocation) {
|
||||
classesLocation = (classesLocation != null ? classesLocation : "");
|
||||
Enumeration<JarEntry> sourceEntries = source.entries();
|
||||
List<JarEntry> classEntries = new ArrayList<>();
|
||||
while (sourceEntries.hasMoreElements()) {
|
||||
JarEntry entry = sourceEntries.nextElement();
|
||||
if (entry.getName().startsWith(classesLocation)
|
||||
&& entry.getName().endsWith(DOT_CLASS)) {
|
||||
classEntries.add(entry);
|
||||
}
|
||||
}
|
||||
return classEntries;
|
||||
}
|
||||
|
||||
private static ClassDescriptor createClassDescriptor(InputStream inputStream) {
|
||||
try {
|
||||
ClassReader classReader = new ClassReader(inputStream);
|
||||
ClassDescriptor classDescriptor = new ClassDescriptor();
|
||||
classReader.accept(classDescriptor, ClassReader.SKIP_CODE);
|
||||
return classDescriptor;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClassEntryComparator implements Comparator<JarEntry> {
|
||||
|
||||
@Override
|
||||
public int compare(JarEntry o1, JarEntry o2) {
|
||||
Integer d1 = getDepth(o1);
|
||||
Integer d2 = getDepth(o2);
|
||||
int depthCompare = d1.compareTo(d2);
|
||||
if (depthCompare != 0) {
|
||||
return depthCompare;
|
||||
}
|
||||
return o1.getName().compareTo(o2.getName());
|
||||
}
|
||||
|
||||
private int getDepth(JarEntry entry) {
|
||||
return entry.getName().split("/").length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ClassDescriptor extends ClassVisitor {
|
||||
|
||||
private final Set<String> annotationNames = new LinkedHashSet<>();
|
||||
|
||||
private boolean mainMethodFound;
|
||||
|
||||
ClassDescriptor() {
|
||||
super(Opcodes.ASM4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
|
||||
this.annotationNames.add(Type.getType(desc).getClassName());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String desc,
|
||||
String signature, String[] exceptions) {
|
||||
if (isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC)
|
||||
&& MAIN_METHOD_NAME.equals(name)
|
||||
&& MAIN_METHOD_TYPE.getDescriptor().equals(desc)) {
|
||||
this.mainMethodFound = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isAccess(int access, int... requiredOpsCodes) {
|
||||
for (int requiredOpsCode : requiredOpsCodes) {
|
||||
if ((access & requiredOpsCode) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isMainMethodFound() {
|
||||
return this.mainMethodFound;
|
||||
}
|
||||
|
||||
Set<String> getAnnotationNames() {
|
||||
return this.annotationNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for handling {@link MainClass MainClasses}.
|
||||
*
|
||||
* @param <T> the callback's return type
|
||||
*/
|
||||
interface MainClassCallback<T> {
|
||||
|
||||
/**
|
||||
* Handle the specified main class.
|
||||
* @param mainClass the main class
|
||||
* @return a non-null value if processing should end or {@code null} to continue
|
||||
*/
|
||||
T doWith(MainClass mainClass);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A class with a {@code main} method.
|
||||
*/
|
||||
static final class MainClass {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Set<String> annotationNames;
|
||||
|
||||
/**
|
||||
* Creates a new {@code MainClass} rather represents the main class with the given
|
||||
* {@code name}. The class is annotated with the annotations with the given
|
||||
* {@code annotationNames}.
|
||||
* @param name the name of the class
|
||||
* @param annotationNames the names of the annotations on the class
|
||||
*/
|
||||
MainClass(String name, Set<String> annotationNames) {
|
||||
this.name = name;
|
||||
this.annotationNames = Collections
|
||||
.unmodifiableSet(new HashSet<>(annotationNames));
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
Set<String> getAnnotationNames() {
|
||||
return this.annotationNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MainClass other = (MainClass) obj;
|
||||
if (!this.name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single main class, throwing an {@link IllegalStateException} if multiple
|
||||
* candidates exist.
|
||||
*/
|
||||
private static final class SingleMainClassCallback
|
||||
implements MainClassCallback<Object> {
|
||||
|
||||
private final Set<MainClass> mainClasses = new LinkedHashSet<>();
|
||||
|
||||
private final String annotationName;
|
||||
|
||||
private SingleMainClassCallback(String annotationName) {
|
||||
this.annotationName = annotationName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doWith(MainClass mainClass) {
|
||||
this.mainClasses.add(mainClass);
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getMainClassName() {
|
||||
Set<MainClass> matchingMainClasses = new LinkedHashSet<>();
|
||||
if (this.annotationName != null) {
|
||||
for (MainClass mainClass : this.mainClasses) {
|
||||
if (mainClass.getAnnotationNames().contains(this.annotationName)) {
|
||||
matchingMainClasses.add(mainClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchingMainClasses.isEmpty()) {
|
||||
matchingMainClasses.addAll(this.mainClasses);
|
||||
}
|
||||
if (matchingMainClasses.size() > 1) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to find a single main class from the following candidates "
|
||||
+ matchingMainClasses);
|
||||
}
|
||||
return matchingMainClasses.isEmpty() ? null
|
||||
: matchingMainClasses.iterator().next().getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
|
||||
|
||||
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility class that can be used to repackage an archive so that it can be executed using
|
||||
* '{@literal java -jar}'.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class Repackager {
|
||||
|
||||
private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
|
||||
|
||||
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
|
||||
|
||||
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
|
||||
|
||||
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
|
||||
|
||||
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";
|
||||
|
||||
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
|
||||
|
||||
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
|
||||
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
|
||||
|
||||
private List<MainClassTimeoutWarningListener> mainClassTimeoutListeners = new ArrayList<>();
|
||||
|
||||
private String mainClass;
|
||||
|
||||
private boolean backupSource = true;
|
||||
|
||||
private final File source;
|
||||
|
||||
private Layout layout;
|
||||
|
||||
private LayoutFactory layoutFactory;
|
||||
|
||||
public Repackager(File source) {
|
||||
this(source, null);
|
||||
}
|
||||
|
||||
public Repackager(File source, LayoutFactory layoutFactory) {
|
||||
if (source == null) {
|
||||
throw new IllegalArgumentException("Source file must be provided");
|
||||
}
|
||||
if (!source.exists() || !source.isFile()) {
|
||||
throw new IllegalArgumentException("Source must refer to an existing file, "
|
||||
+ "got " + source.getAbsolutePath());
|
||||
}
|
||||
this.source = source.getAbsoluteFile();
|
||||
this.layoutFactory = layoutFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener that will be triggered to display a warning if searching for the
|
||||
* main class takes too long.
|
||||
* @param listener the listener to add
|
||||
*/
|
||||
public void addMainClassTimeoutWarningListener(
|
||||
MainClassTimeoutWarningListener listener) {
|
||||
this.mainClassTimeoutListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the main class that should be run. If not specified the value from the
|
||||
* MANIFEST will be used, or if no manifest entry is found the archive will be
|
||||
* searched for a suitable class.
|
||||
* @param mainClass the main class name
|
||||
*/
|
||||
public void setMainClass(String mainClass) {
|
||||
this.mainClass = mainClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if source files should be backed up when they would be overwritten.
|
||||
* @param backupSource if source files should be backed up
|
||||
*/
|
||||
public void setBackupSource(boolean backupSource) {
|
||||
this.backupSource = backupSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}.
|
||||
* @param layout the layout
|
||||
*/
|
||||
public void setLayout(Layout layout) {
|
||||
if (layout == null) {
|
||||
throw new IllegalArgumentException("Layout must not be null");
|
||||
}
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layout factory for the jar. The factory can be used when no specific
|
||||
* layout is specified.
|
||||
* @param layoutFactory the layout factory to set
|
||||
*/
|
||||
public void setLayoutFactory(LayoutFactory layoutFactory) {
|
||||
this.layoutFactory = layoutFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repackage the source file so that it can be run using '{@literal java -jar}'.
|
||||
* @param libraries the libraries required to run the archive
|
||||
* @throws IOException if the file cannot be repackaged
|
||||
*/
|
||||
public void repackage(Libraries libraries) throws IOException {
|
||||
repackage(this.source, libraries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repackage to the given destination so that it can be launched using '
|
||||
* {@literal java -jar}'.
|
||||
* @param destination the destination file (may be the same as the source)
|
||||
* @param libraries the libraries required to run the archive
|
||||
* @throws IOException if the file cannot be repackaged
|
||||
*/
|
||||
public void repackage(File destination, Libraries libraries) throws IOException {
|
||||
repackage(destination, libraries, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repackage to the given destination so that it can be launched using '
|
||||
* {@literal java -jar}'.
|
||||
* @param destination the destination file (may be the same as the source)
|
||||
* @param libraries the libraries required to run the archive
|
||||
* @param launchScript an optional launch script prepended to the front of the jar
|
||||
* @throws IOException if the file cannot be repackaged
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public void repackage(File destination, Libraries libraries,
|
||||
LaunchScript launchScript) throws IOException {
|
||||
if (destination == null || destination.isDirectory()) {
|
||||
throw new IllegalArgumentException("Invalid destination");
|
||||
}
|
||||
if (libraries == null) {
|
||||
throw new IllegalArgumentException("Libraries must not be null");
|
||||
}
|
||||
if (this.layout == null) {
|
||||
this.layout = getLayoutFactory().getLayout(this.source);
|
||||
}
|
||||
if (alreadyRepackaged()) {
|
||||
return;
|
||||
}
|
||||
destination = destination.getAbsoluteFile();
|
||||
File workingSource = this.source;
|
||||
if (this.source.equals(destination)) {
|
||||
workingSource = getBackupFile();
|
||||
workingSource.delete();
|
||||
renameFile(this.source, workingSource);
|
||||
}
|
||||
destination.delete();
|
||||
try {
|
||||
try (JarFile jarFileSource = new JarFile(workingSource)) {
|
||||
repackage(jarFileSource, destination, libraries, launchScript);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (!this.backupSource && !this.source.equals(workingSource)) {
|
||||
deleteFile(workingSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LayoutFactory getLayoutFactory() {
|
||||
if (this.layoutFactory != null) {
|
||||
return this.layoutFactory;
|
||||
}
|
||||
List<LayoutFactory> factories = SpringFactoriesLoader
|
||||
.loadFactories(LayoutFactory.class, null);
|
||||
if (factories.isEmpty()) {
|
||||
return new DefaultLayoutFactory();
|
||||
}
|
||||
Assert.state(factories.size() == 1, "No unique LayoutFactory found");
|
||||
return factories.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link File} to use to backup the original source.
|
||||
* @return the file to use to backup the original source
|
||||
*/
|
||||
public final File getBackupFile() {
|
||||
return new File(this.source.getParentFile(), this.source.getName() + ".original");
|
||||
}
|
||||
|
||||
private boolean alreadyRepackaged() throws IOException {
|
||||
try (JarFile jarFile = new JarFile(this.source)) {
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
return (manifest != null && manifest.getMainAttributes()
|
||||
.getValue(BOOT_VERSION_ATTRIBUTE) != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void repackage(JarFile sourceJar, File destination, Libraries libraries,
|
||||
LaunchScript launchScript) throws IOException {
|
||||
try (JarWriter writer = new JarWriter(destination, launchScript)) {
|
||||
final List<Library> unpackLibraries = new ArrayList<>();
|
||||
final List<Library> standardLibraries = new ArrayList<>();
|
||||
libraries.doWithLibraries((library) -> {
|
||||
File file = library.getFile();
|
||||
if (isZip(file)) {
|
||||
if (library.isUnpackRequired()) {
|
||||
unpackLibraries.add(library);
|
||||
}
|
||||
else {
|
||||
standardLibraries.add(library);
|
||||
}
|
||||
}
|
||||
});
|
||||
repackage(sourceJar, writer, unpackLibraries, standardLibraries);
|
||||
}
|
||||
}
|
||||
|
||||
private void repackage(JarFile sourceJar, JarWriter writer,
|
||||
final List<Library> unpackLibraries, final List<Library> standardLibraries)
|
||||
throws IOException {
|
||||
writer.writeManifest(buildManifest(sourceJar));
|
||||
Set<String> seen = new HashSet<>();
|
||||
writeNestedLibraries(unpackLibraries, seen, writer);
|
||||
if (this.layout instanceof RepackagingLayout) {
|
||||
writer.writeEntries(sourceJar, new RenamingEntryTransformer(
|
||||
((RepackagingLayout) this.layout).getRepackagedClassesLocation()));
|
||||
}
|
||||
else {
|
||||
writer.writeEntries(sourceJar);
|
||||
}
|
||||
writeNestedLibraries(standardLibraries, seen, writer);
|
||||
writeLoaderClasses(writer);
|
||||
}
|
||||
|
||||
private void writeNestedLibraries(List<Library> libraries, Set<String> alreadySeen,
|
||||
JarWriter writer) throws IOException {
|
||||
for (Library library : libraries) {
|
||||
String destination = Repackager.this.layout
|
||||
.getLibraryDestination(library.getName(), library.getScope());
|
||||
if (destination != null) {
|
||||
if (!alreadySeen.add(destination + library.getName())) {
|
||||
throw new IllegalStateException(
|
||||
"Duplicate library " + library.getName());
|
||||
}
|
||||
writer.writeNestedLibrary(destination, library);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLoaderClasses(JarWriter writer) throws IOException {
|
||||
if (this.layout instanceof CustomLoaderLayout) {
|
||||
((CustomLoaderLayout) this.layout).writeLoadedClasses(writer);
|
||||
}
|
||||
else if (this.layout.isExecutable()) {
|
||||
writer.writeLoaderClasses();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isZip(File file) {
|
||||
try {
|
||||
try (FileInputStream fileInputStream = new FileInputStream(file)) {
|
||||
return isZip(fileInputStream);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isZip(InputStream inputStream) throws IOException {
|
||||
for (int i = 0; i < ZIP_FILE_HEADER.length; i++) {
|
||||
if (inputStream.read() != ZIP_FILE_HEADER[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Manifest buildManifest(JarFile source) throws IOException {
|
||||
Manifest manifest = source.getManifest();
|
||||
if (manifest == null) {
|
||||
manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
|
||||
}
|
||||
manifest = new Manifest(manifest);
|
||||
String startClass = this.mainClass;
|
||||
if (startClass == null) {
|
||||
startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
|
||||
}
|
||||
if (startClass == null) {
|
||||
startClass = findMainMethodWithTimeoutWarning(source);
|
||||
}
|
||||
String launcherClassName = this.layout.getLauncherClassName();
|
||||
if (launcherClassName != null) {
|
||||
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE,
|
||||
launcherClassName);
|
||||
if (startClass == null) {
|
||||
throw new IllegalStateException("Unable to find main class");
|
||||
}
|
||||
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass);
|
||||
}
|
||||
else if (startClass != null) {
|
||||
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass);
|
||||
}
|
||||
String bootVersion = getClass().getPackage().getImplementationVersion();
|
||||
manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion);
|
||||
manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE,
|
||||
(this.layout instanceof RepackagingLayout)
|
||||
? ((RepackagingLayout) this.layout).getRepackagedClassesLocation()
|
||||
: this.layout.getClassesLocation());
|
||||
String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE);
|
||||
if (StringUtils.hasLength(lib)) {
|
||||
manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String mainMethod = findMainMethod(source);
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
if (duration > FIND_WARNING_TIMEOUT) {
|
||||
for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) {
|
||||
listener.handleTimeoutWarning(duration, mainMethod);
|
||||
}
|
||||
}
|
||||
return mainMethod;
|
||||
}
|
||||
|
||||
protected String findMainMethod(JarFile source) throws IOException {
|
||||
return MainClassFinder.findSingleMainClass(source,
|
||||
this.layout.getClassesLocation(), SPRING_BOOT_APPLICATION_CLASS_NAME);
|
||||
}
|
||||
|
||||
private void renameFile(File file, File dest) {
|
||||
if (!file.renameTo(dest)) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to rename '" + file + "' to '" + dest + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFile(File file) {
|
||||
if (!file.delete()) {
|
||||
throw new IllegalStateException("Unable to delete '" + file + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface used to present a warning when finding the main class takes too
|
||||
* long.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MainClassTimeoutWarningListener {
|
||||
|
||||
/**
|
||||
* Handle a timeout warning.
|
||||
* @param duration the amount of time it took to find the main method
|
||||
* @param mainMethod the main method that was actually found
|
||||
*/
|
||||
void handleTimeoutWarning(long duration, String mainMethod);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EntryTransformer} that renames entries by applying a prefix.
|
||||
*/
|
||||
private static final class RenamingEntryTransformer implements EntryTransformer {
|
||||
|
||||
private final String namePrefix;
|
||||
|
||||
private RenamingEntryTransformer(String namePrefix) {
|
||||
this.namePrefix = namePrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarArchiveEntry transform(JarArchiveEntry entry) {
|
||||
if (entry.getName().equals("META-INF/INDEX.LIST")) {
|
||||
return null;
|
||||
}
|
||||
if ((entry.getName().startsWith("META-INF/")
|
||||
&& !entry.getName().equals("META-INF/aop.xml"))
|
||||
|| entry.getName().startsWith("BOOT-INF/")) {
|
||||
return entry;
|
||||
}
|
||||
JarArchiveEntry renamedEntry = new JarArchiveEntry(
|
||||
this.namePrefix + entry.getName());
|
||||
renamedEntry.setTime(entry.getTime());
|
||||
renamedEntry.setSize(entry.getSize());
|
||||
renamedEntry.setMethod(entry.getMethod());
|
||||
if (entry.getComment() != null) {
|
||||
renamedEntry.setComment(entry.getComment());
|
||||
}
|
||||
renamedEntry.setCompressedSize(entry.getCompressedSize());
|
||||
renamedEntry.setCrc(entry.getCrc());
|
||||
if (entry.getCreationTime() != null) {
|
||||
renamedEntry.setCreationTime(entry.getCreationTime());
|
||||
}
|
||||
if (entry.getExtra() != null) {
|
||||
renamedEntry.setExtra(entry.getExtra());
|
||||
}
|
||||
if (entry.getLastAccessTime() != null) {
|
||||
renamedEntry.setLastAccessTime(entry.getLastAccessTime());
|
||||
}
|
||||
if (entry.getLastModifiedTime() != null) {
|
||||
renamedEntry.setLastModifiedTime(entry.getLastModifiedTime());
|
||||
}
|
||||
return renamedEntry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
/**
|
||||
* A specialization of {@link Layout} that repackages an existing archive by moving its
|
||||
* content to a new location.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public interface RepackagingLayout extends Layout {
|
||||
|
||||
/**
|
||||
* Returns the location to which classes should be moved.
|
||||
* @return the repackaged classes location
|
||||
*/
|
||||
String getRepackagedClassesLocation();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Utility used to run a process.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public class RunProcess {
|
||||
|
||||
private static final Method INHERIT_IO_METHOD = ReflectionUtils
|
||||
.findMethod(ProcessBuilder.class, "inheritIO");
|
||||
|
||||
private static final long JUST_ENDED_LIMIT = 500;
|
||||
|
||||
private File workingDirectory;
|
||||
|
||||
private final String[] command;
|
||||
|
||||
private volatile Process process;
|
||||
|
||||
private volatile long endTime;
|
||||
|
||||
/**
|
||||
* Creates new {@link RunProcess} instance for the specified command.
|
||||
* @param command the program to execute and its arguments
|
||||
*/
|
||||
public RunProcess(String... command) {
|
||||
this(null, command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RunProcess} instance for the specified working directory and
|
||||
* command.
|
||||
* @param workingDirectory the working directory of the child process or {@code null}
|
||||
* to run in the working directory of the current Java process
|
||||
* @param command the program to execute and its arguments
|
||||
*/
|
||||
public RunProcess(File workingDirectory, String... command) {
|
||||
this.workingDirectory = workingDirectory;
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public int run(boolean waitForProcess, String... args) throws IOException {
|
||||
return run(waitForProcess, Arrays.asList(args));
|
||||
}
|
||||
|
||||
protected int run(boolean waitForProcess, Collection<String> args)
|
||||
throws IOException {
|
||||
ProcessBuilder builder = new ProcessBuilder(this.command);
|
||||
builder.directory(this.workingDirectory);
|
||||
builder.command().addAll(args);
|
||||
builder.redirectErrorStream(true);
|
||||
boolean inheritedIO = inheritIO(builder);
|
||||
try {
|
||||
Process process = builder.start();
|
||||
this.process = process;
|
||||
if (!inheritedIO) {
|
||||
redirectOutput(process);
|
||||
}
|
||||
SignalUtils.attachSignalHandler(this::handleSigInt);
|
||||
if (waitForProcess) {
|
||||
try {
|
||||
return process.waitFor();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
finally {
|
||||
if (waitForProcess) {
|
||||
this.endTime = System.currentTimeMillis();
|
||||
this.process = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean inheritIO(ProcessBuilder builder) {
|
||||
if (isInheritIOBroken()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
INHERIT_IO_METHOD.invoke(builder);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// There's a bug in the Windows VM (https://bugs.openjdk.java.net/browse/JDK-8023130)
|
||||
// that means we need to avoid inheritIO
|
||||
private static boolean isInheritIOBroken() {
|
||||
if (!System.getProperty("os.name", "none").toLowerCase().contains("windows")) {
|
||||
return false;
|
||||
}
|
||||
String runtime = System.getProperty("java.runtime.version");
|
||||
if (!runtime.startsWith("1.7")) {
|
||||
return false;
|
||||
}
|
||||
String[] tokens = runtime.split("_");
|
||||
if (tokens.length < 2) {
|
||||
return true; // No idea actually, shouldn't happen
|
||||
}
|
||||
try {
|
||||
Integer build = Integer.valueOf(tokens[1].split("[^0-9]")[0]);
|
||||
if (build < 60) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void redirectOutput(Process process) {
|
||||
final BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()));
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String line = reader.readLine();
|
||||
while (line != null) {
|
||||
System.out.println(line);
|
||||
line = reader.readLine();
|
||||
System.out.flush();
|
||||
}
|
||||
reader.close();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the running process.
|
||||
* @return the process or {@code null}
|
||||
*/
|
||||
public Process getRunningProcess() {
|
||||
return this.process;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the process was stopped.
|
||||
* @return {@code true} if stopped
|
||||
*/
|
||||
public boolean handleSigInt() {
|
||||
// if the process has just ended, probably due to this SIGINT, consider handled.
|
||||
if (hasJustEnded()) {
|
||||
return true;
|
||||
}
|
||||
return doKill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill this process.
|
||||
*/
|
||||
public void kill() {
|
||||
doKill();
|
||||
}
|
||||
|
||||
private boolean doKill() {
|
||||
// destroy the running process
|
||||
Process process = this.process;
|
||||
if (process != null) {
|
||||
try {
|
||||
process.destroy();
|
||||
process.waitFor();
|
||||
this.process = null;
|
||||
return true;
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasJustEnded() {
|
||||
return System.currentTimeMillis() < (this.endTime + JUST_ENDED_LIMIT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import sun.misc.Signal;
|
||||
|
||||
/**
|
||||
* Utilities for working with signal handling.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@SuppressWarnings("restriction")
|
||||
@UsesUnsafeJava
|
||||
public final class SignalUtils {
|
||||
|
||||
private static final Signal SIG_INT = new Signal("INT");
|
||||
|
||||
private SignalUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle {@literal INT} signals by calling the specified {@link Runnable}.
|
||||
* @param runnable the runnable to call on SIGINT.
|
||||
*/
|
||||
public static void attachSignalHandler(final Runnable runnable) {
|
||||
Signal.handle(SIG_INT, (signal) -> runnable.run());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated element uses unsafe Java calls.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE })
|
||||
@Documented
|
||||
@interface UsesUnsafeJava {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tools for generating executable JAR/WAR files.
|
||||
*/
|
||||
package org.springframework.boot.loader.tools;
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# . ____ _ __ _ _
|
||||
# /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
# ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
# \\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
# ' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
# =========|_|==============|___/=/_/_/_/
|
||||
# :: Spring Boot Startup Script ::
|
||||
#
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{initInfoProvides:spring-boot-application}}
|
||||
# Required-Start: {{initInfoRequiredStart:$remote_fs $syslog $network}}
|
||||
# Required-Stop: {{initInfoRequiredStop:$remote_fs $syslog $network}}
|
||||
# Default-Start: {{initInfoDefaultStart:2 3 4 5}}
|
||||
# Default-Stop: {{initInfoDefaultStop:0 1 6}}
|
||||
# Short-Description: {{initInfoShortDescription:Spring Boot Application}}
|
||||
# Description: {{initInfoDescription:Spring Boot Application}}
|
||||
# chkconfig: {{initInfoChkconfig:2345 99 01}}
|
||||
### END INIT INFO
|
||||
|
||||
[[ -n "$DEBUG" ]] && set -x
|
||||
|
||||
# Initialize variables that cannot be provided by a .conf file
|
||||
WORKING_DIR="$(pwd)"
|
||||
# shellcheck disable=SC2153
|
||||
[[ -n "$JARFILE" ]] && jarfile="$JARFILE"
|
||||
[[ -n "$APP_NAME" ]] && identity="$APP_NAME"
|
||||
|
||||
# Follow symlinks to find the real jar and detect init.d script
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
[[ -z "$jarfile" ]] && jarfile=$(pwd)/$(basename "$0")
|
||||
while [[ -L "$jarfile" ]]; do
|
||||
if [[ "$jarfile" =~ init\.d ]]; then
|
||||
init_script=$(basename "$jarfile")
|
||||
else
|
||||
configfile="${jarfile%.*}.conf"
|
||||
# shellcheck source=/dev/null
|
||||
[[ -r ${configfile} ]] && source "${configfile}"
|
||||
fi
|
||||
jarfile=$(readlink "$jarfile")
|
||||
cd "$(dirname "$jarfile")" || exit 1
|
||||
jarfile=$(pwd)/$(basename "$jarfile")
|
||||
done
|
||||
jarfolder="$( (cd "$(dirname "$jarfile")" && pwd -P) )"
|
||||
cd "$WORKING_DIR" || exit 1
|
||||
|
||||
# Inline script specified in build properties
|
||||
{{inlinedConfScript:}}
|
||||
|
||||
# Source any config file
|
||||
configfile="$(basename "${jarfile%.*}.conf")"
|
||||
|
||||
# Initialize CONF_FOLDER location defaulting to jarfolder
|
||||
[[ -z "$CONF_FOLDER" ]] && CONF_FOLDER="{{confFolder:${jarfolder}}}"
|
||||
# shellcheck source=/dev/null
|
||||
[[ -r "${CONF_FOLDER}/${configfile}" ]] && source "${CONF_FOLDER}/${configfile}"
|
||||
|
||||
# Initialize PID/LOG locations if they weren't provided by the config file
|
||||
[[ -z "$PID_FOLDER" ]] && PID_FOLDER="{{pidFolder:/var/run}}"
|
||||
[[ -z "$LOG_FOLDER" ]] && LOG_FOLDER="{{logFolder:/var/log}}"
|
||||
! [[ "$PID_FOLDER" == /* ]] && PID_FOLDER="$(dirname "$jarfile")"/"$PID_FOLDER"
|
||||
! [[ "$LOG_FOLDER" == /* ]] && LOG_FOLDER="$(dirname "$jarfile")"/"$LOG_FOLDER"
|
||||
! [[ -x "$PID_FOLDER" ]] && PID_FOLDER="/tmp"
|
||||
! [[ -x "$LOG_FOLDER" ]] && LOG_FOLDER="/tmp"
|
||||
|
||||
# Set up defaults
|
||||
[[ -z "$MODE" ]] && MODE="{{mode:auto}}" # modes are "auto", "service" or "run"
|
||||
[[ -z "$USE_START_STOP_DAEMON" ]] && USE_START_STOP_DAEMON="{{useStartStopDaemon:true}}"
|
||||
|
||||
# Create an identity for log/pid files
|
||||
if [[ -z "$identity" ]]; then
|
||||
if [[ -n "$init_script" ]]; then
|
||||
identity="${init_script}"
|
||||
else
|
||||
identity=$(basename "${jarfile%.*}")_${jarfolder//\//}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize log file name if not provided by the config file
|
||||
[[ -z "$LOG_FILENAME" ]] && LOG_FILENAME="{{logFilename:${identity}.log}}"
|
||||
|
||||
# Initialize stop wait time if not provided by the config file
|
||||
[[ -z "$STOP_WAIT_TIME" ]] && STOP_WAIT_TIME="{{stopWaitTime:60}}"
|
||||
|
||||
# ANSI Colors
|
||||
echoRed() { echo $'\e[0;31m'"$1"$'\e[0m'; }
|
||||
echoGreen() { echo $'\e[0;32m'"$1"$'\e[0m'; }
|
||||
echoYellow() { echo $'\e[0;33m'"$1"$'\e[0m'; }
|
||||
|
||||
# Utility functions
|
||||
checkPermissions() {
|
||||
touch "$pid_file" &> /dev/null || { echoRed "Operation not permitted (cannot access pid file)"; return 4; }
|
||||
touch "$log_file" &> /dev/null || { echoRed "Operation not permitted (cannot access log file)"; return 4; }
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
ps -p "$1" &> /dev/null
|
||||
}
|
||||
|
||||
await_file() {
|
||||
end=$(date +%s)
|
||||
let "end+=10"
|
||||
while [[ ! -s "$1" ]]
|
||||
do
|
||||
now=$(date +%s)
|
||||
if [[ $now -ge $end ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# Determine the script mode
|
||||
action="run"
|
||||
if [[ "$MODE" == "auto" && -n "$init_script" ]] || [[ "$MODE" == "service" ]]; then
|
||||
action="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Build the pid and log filenames
|
||||
PID_FOLDER="$PID_FOLDER/${identity}"
|
||||
pid_file="$PID_FOLDER/{{pidFilename:${identity}.pid}}"
|
||||
log_file="$LOG_FOLDER/$LOG_FILENAME"
|
||||
|
||||
# Determine the user to run as if we are root
|
||||
# shellcheck disable=SC2012
|
||||
[[ $(id -u) == "0" ]] && run_user=$(ls -ld "$jarfile" | awk '{print $3}')
|
||||
|
||||
# Find Java
|
||||
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
javaexe="$JAVA_HOME/bin/java"
|
||||
elif type -p java > /dev/null 2>&1; then
|
||||
javaexe=$(type -p java)
|
||||
elif [[ -x "/usr/bin/java" ]]; then
|
||||
javaexe="/usr/bin/java"
|
||||
else
|
||||
echo "Unable to find Java"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
arguments=(-Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar "$jarfile" $RUN_ARGS "$@")
|
||||
|
||||
# Action functions
|
||||
start() {
|
||||
if [[ -f "$pid_file" ]]; then
|
||||
pid=$(cat "$pid_file")
|
||||
isRunning "$pid" && { echoYellow "Already running [$pid]"; return 0; }
|
||||
fi
|
||||
do_start "$@"
|
||||
}
|
||||
|
||||
do_start() {
|
||||
working_dir=$(dirname "$jarfile")
|
||||
pushd "$working_dir" > /dev/null
|
||||
mkdir -p "$PID_FOLDER" &> /dev/null
|
||||
if [[ -n "$run_user" ]]; then
|
||||
checkPermissions || return $?
|
||||
chown "$run_user" "$PID_FOLDER"
|
||||
chown "$run_user" "$pid_file"
|
||||
chown "$run_user" "$log_file"
|
||||
if [ $USE_START_STOP_DAEMON = true ] && type start-stop-daemon > /dev/null 2>&1; then
|
||||
start-stop-daemon --start --quiet \
|
||||
--chuid "$run_user" \
|
||||
--name "$identity" \
|
||||
--make-pidfile --pidfile "$pid_file" \
|
||||
--background --no-close \
|
||||
--startas "$javaexe" \
|
||||
--chdir "$working_dir" \
|
||||
-- "${arguments[@]}" \
|
||||
>> "$log_file" 2>&1
|
||||
await_file "$pid_file"
|
||||
else
|
||||
su -s /bin/sh -c "$javaexe $(printf "\"%s\" " "${arguments[@]}") >> \"$log_file\" 2>&1 & echo \$!" "$run_user" > "$pid_file"
|
||||
fi
|
||||
pid=$(cat "$pid_file")
|
||||
else
|
||||
checkPermissions || return $?
|
||||
"$javaexe" "${arguments[@]}" >> "$log_file" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
echo "$pid" > "$pid_file"
|
||||
fi
|
||||
[[ -z $pid ]] && { echoRed "Failed to start"; return 1; }
|
||||
echoGreen "Started [$pid]"
|
||||
}
|
||||
|
||||
stop() {
|
||||
working_dir=$(dirname "$jarfile")
|
||||
pushd "$working_dir" > /dev/null
|
||||
[[ -f $pid_file ]] || { echoYellow "Not running (pidfile not found)"; return 0; }
|
||||
pid=$(cat "$pid_file")
|
||||
isRunning "$pid" || { echoYellow "Not running (process ${pid}). Removing stale pid file."; rm -f "$pid_file"; return 0; }
|
||||
do_stop "$pid" "$pid_file"
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
kill "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; }
|
||||
for i in $(seq 1 $STOP_WAIT_TIME); do
|
||||
isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; }
|
||||
[[ $i -eq STOP_WAIT_TIME/2 ]] && kill "$1" &> /dev/null
|
||||
sleep 1
|
||||
done
|
||||
echoRed "Unable to kill process $1";
|
||||
return 1;
|
||||
}
|
||||
|
||||
force_stop() {
|
||||
[[ -f $pid_file ]] || { echoYellow "Not running (pidfile not found)"; return 0; }
|
||||
pid=$(cat "$pid_file")
|
||||
isRunning "$pid" || { echoYellow "Not running (process ${pid}). Removing stale pid file."; rm -f "$pid_file"; return 0; }
|
||||
do_force_stop "$pid" "$pid_file"
|
||||
}
|
||||
|
||||
do_force_stop() {
|
||||
kill -9 "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; }
|
||||
for i in $(seq 1 $STOP_WAIT_TIME); do
|
||||
isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; }
|
||||
[[ $i -eq STOP_WAIT_TIME/2 ]] && kill -9 "$1" &> /dev/null
|
||||
sleep 1
|
||||
done
|
||||
echoRed "Unable to kill process $1";
|
||||
return 1;
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop && start
|
||||
}
|
||||
|
||||
force_reload() {
|
||||
working_dir=$(dirname "$jarfile")
|
||||
pushd "$working_dir" > /dev/null
|
||||
[[ -f $pid_file ]] || { echoRed "Not running (pidfile not found)"; return 7; }
|
||||
pid=$(cat "$pid_file")
|
||||
rm -f "$pid_file"
|
||||
isRunning "$pid" || { echoRed "Not running (process ${pid} not found)"; return 7; }
|
||||
do_stop "$pid" "$pid_file"
|
||||
do_start
|
||||
}
|
||||
|
||||
status() {
|
||||
working_dir=$(dirname "$jarfile")
|
||||
pushd "$working_dir" > /dev/null
|
||||
[[ -f "$pid_file" ]] || { echoRed "Not running"; return 3; }
|
||||
pid=$(cat "$pid_file")
|
||||
isRunning "$pid" || { echoRed "Not running (process ${pid} not found)"; return 1; }
|
||||
echoGreen "Running [$pid]"
|
||||
return 0
|
||||
}
|
||||
|
||||
run() {
|
||||
pushd "$(dirname "$jarfile")" > /dev/null
|
||||
"$javaexe" "${arguments[@]}"
|
||||
result=$?
|
||||
popd > /dev/null
|
||||
return "$result"
|
||||
}
|
||||
|
||||
# Call the appropriate action function
|
||||
case "$action" in
|
||||
start)
|
||||
start "$@"; exit $?;;
|
||||
stop)
|
||||
stop "$@"; exit $?;;
|
||||
force-stop)
|
||||
force_stop "$@"; exit $?;;
|
||||
restart)
|
||||
restart "$@"; exit $?;;
|
||||
force-reload)
|
||||
force_reload "$@"; exit $?;;
|
||||
status)
|
||||
status "$@"; exit $?;;
|
||||
run)
|
||||
run "$@"; exit $?;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|force-stop|restart|force-reload|status|run}"; exit 1;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultLaunchScript}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Justin Rosenberg
|
||||
*/
|
||||
public class DefaultLaunchScriptTests {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void loadsDefaultScript() throws Exception {
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).contains("Spring Boot Startup Script");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logFilenameCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("logFilename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pidFilenameCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("pidFilename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoProvidesCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoProvides");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoRequiredStartCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoRequiredStart");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoRequiredStopCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoRequiredStop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoDefaultStartCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoDefaultStart");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoDefaultStopCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoDefaultStop");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoShortDescriptionCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoShortDescription");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoDescriptionCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoDescription");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initInfoChkconfigCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("initInfoChkconfig");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modeCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("mode");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useStartStopDaemonCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("useStartStopDaemon");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logFolderCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("logFolder");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pidFolderCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("pidFolder");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confFolderCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("confFolder");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stopWaitTimeCanBeReplaced() throws Exception {
|
||||
assertThatPlaceholderCanBeReplaced("stopWaitTime");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlinedConfScriptFileLoad() throws IOException {
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(null,
|
||||
createProperties("inlinedConfScript:src/test/resources/example.script"));
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).contains("FOO=BAR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultForUseStartStopDaemonIsTrue() throws Exception {
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).contains("USE_START_STOP_DAEMON=\"true\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultForModeIsAuto() throws Exception {
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).contains("MODE=\"auto\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultForStopWaitTimeIs60() throws Exception {
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).contains("STOP_WAIT_TIME=\"60\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadFromFile() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("ABC".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("ABC");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandVariables() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("h{{a}}ll{{b}}".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file,
|
||||
createProperties("a:e", "b:o"));
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandVariablesMultiLine() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("h{{a}}l\nl{{b}}".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file,
|
||||
createProperties("a:e", "b:o"));
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("hel\nlo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandVariablesWithDefaults() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("h{{a:e}}ll{{b:o}}".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandVariablesCanDefaultToBlank() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("s{{p:}}{{r:}}ing".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("sing");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandVariablesWithDefaultsOverride() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("h{{a:e}}ll{{b:o}}".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file,
|
||||
createProperties("a:a"));
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("hallo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandVariablesMissingAreUnchanged() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy("h{{a}}ll{{b}}".getBytes(), file);
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).isEqualTo("h{{a}}ll{{b}}");
|
||||
}
|
||||
|
||||
private void assertThatPlaceholderCanBeReplaced(String placeholder) throws Exception {
|
||||
DefaultLaunchScript script = new DefaultLaunchScript(null,
|
||||
createProperties(placeholder + ":__test__"));
|
||||
String content = new String(script.toByteArray());
|
||||
assertThat(content).contains("__test__");
|
||||
}
|
||||
|
||||
private Map<?, ?> createProperties(String... pairs) {
|
||||
Map<Object, Object> properties = new HashMap<>();
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
properties.put(keyValue[0], keyValue[1]);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link FileUtils}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class FileUtilsTests {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private File outputDirectory;
|
||||
|
||||
private File originDirectory;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
this.outputDirectory = new File("target/test/remove");
|
||||
this.originDirectory = new File("target/test/keep");
|
||||
FileSystemUtils.deleteRecursively(this.outputDirectory);
|
||||
FileSystemUtils.deleteRecursively(this.originDirectory);
|
||||
this.outputDirectory.mkdirs();
|
||||
this.originDirectory.mkdirs();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleDuplicateFile() throws IOException {
|
||||
File file = new File(this.outputDirectory, "logback.xml");
|
||||
file.createNewFile();
|
||||
new File(this.originDirectory, "logback.xml").createNewFile();
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(this.outputDirectory,
|
||||
this.originDirectory);
|
||||
assertThat(file.exists()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedDuplicateFile() throws IOException {
|
||||
assertThat(new File(this.outputDirectory, "sub").mkdirs()).isTrue();
|
||||
assertThat(new File(this.originDirectory, "sub").mkdirs()).isTrue();
|
||||
File file = new File(this.outputDirectory, "sub/logback.xml");
|
||||
file.createNewFile();
|
||||
new File(this.originDirectory, "sub/logback.xml").createNewFile();
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(this.outputDirectory,
|
||||
this.originDirectory);
|
||||
assertThat(file.exists()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedNonDuplicateFile() throws IOException {
|
||||
assertThat(new File(this.outputDirectory, "sub").mkdirs()).isTrue();
|
||||
assertThat(new File(this.originDirectory, "sub").mkdirs()).isTrue();
|
||||
File file = new File(this.outputDirectory, "sub/logback.xml");
|
||||
file.createNewFile();
|
||||
new File(this.originDirectory, "sub/different.xml").createNewFile();
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(this.outputDirectory,
|
||||
this.originDirectory);
|
||||
assertThat(file.exists()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonDuplicateFile() throws IOException {
|
||||
File file = new File(this.outputDirectory, "logback.xml");
|
||||
file.createNewFile();
|
||||
new File(this.originDirectory, "different.xml").createNewFile();
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(this.outputDirectory,
|
||||
this.originDirectory);
|
||||
assertThat(file.exists()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hash() throws Exception {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
try (OutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(new byte[] { 1, 2, 3 });
|
||||
}
|
||||
assertThat(FileUtils.sha1Hash(file))
|
||||
.isEqualTo("7037807198c22a7d2b0807371d763779a84fdfcf");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Layouts}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class LayoutsTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void jarFile() throws Exception {
|
||||
assertThat(Layouts.forFile(new File("test.jar"))).isInstanceOf(Layouts.Jar.class);
|
||||
assertThat(Layouts.forFile(new File("test.JAR"))).isInstanceOf(Layouts.Jar.class);
|
||||
assertThat(Layouts.forFile(new File("test.jAr"))).isInstanceOf(Layouts.Jar.class);
|
||||
assertThat(Layouts.forFile(new File("te.st.jar")))
|
||||
.isInstanceOf(Layouts.Jar.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void warFile() throws Exception {
|
||||
assertThat(Layouts.forFile(new File("test.war"))).isInstanceOf(Layouts.War.class);
|
||||
assertThat(Layouts.forFile(new File("test.WAR"))).isInstanceOf(Layouts.War.class);
|
||||
assertThat(Layouts.forFile(new File("test.wAr"))).isInstanceOf(Layouts.War.class);
|
||||
assertThat(Layouts.forFile(new File("te.st.war")))
|
||||
.isInstanceOf(Layouts.War.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownFile() throws Exception {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Unable to deduce layout for 'test.txt'");
|
||||
Layouts.forFile(new File("test.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jarLayout() throws Exception {
|
||||
Layout layout = new Layouts.Jar();
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE))
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM))
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED))
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME))
|
||||
.isEqualTo("BOOT-INF/lib/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void warLayout() throws Exception {
|
||||
Layout layout = new Layouts.War();
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE))
|
||||
.isEqualTo("WEB-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM))
|
||||
.isEqualTo("WEB-INF/lib/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED))
|
||||
.isEqualTo("WEB-INF/lib-provided/");
|
||||
assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME))
|
||||
.isEqualTo("WEB-INF/lib/");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import org.springframework.boot.loader.tools.MainClassFinder.MainClass;
|
||||
import org.springframework.boot.loader.tools.MainClassFinder.MainClassCallback;
|
||||
import org.springframework.boot.loader.tools.sample.AnnotatedClassWithMainMethod;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MainClassFinder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class MainClassFinderTests {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private TestJarFile testJarFile;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
this.testJarFile = new TestJarFile(this.temporaryFolder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMainClassInJar() throws Exception {
|
||||
this.testJarFile.addClass("B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("A.class", ClassWithoutMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(), "");
|
||||
assertThat(actual).isEqualTo("B");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMainClassInJarSubFolder() throws Exception {
|
||||
this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(), "");
|
||||
assertThat(actual).isEqualTo("a.b.c.D");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesBreadthFirstJarSearch() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(), "");
|
||||
assertThat(actual).isEqualTo("a.B");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findSingleJarSearch() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Unable to find a single main class "
|
||||
+ "from the following candidates [a.B, a.b.c.E]");
|
||||
MainClassFinder.findSingleMainClass(this.testJarFile.getJarFile(), "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findSingleJarSearchPrefersAnnotatedMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", AnnotatedClassWithMainMethod.class);
|
||||
String mainClass = MainClassFinder.findSingleMainClass(
|
||||
this.testJarFile.getJarFile(), "",
|
||||
"org.springframework.boot.loader.tools.sample.SomeApplication");
|
||||
assertThat(mainClass).isEqualTo("a.b.c.E");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMainClassInJarSubLocation() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(),
|
||||
"a/");
|
||||
assertThat(actual).isEqualTo("B");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMainClassInFolder() throws Exception {
|
||||
this.testJarFile.addClass("B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("A.class", ClassWithoutMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource());
|
||||
assertThat(actual).isEqualTo("B");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMainClassInSubFolder() throws Exception {
|
||||
this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource());
|
||||
assertThat(actual).isEqualTo("a.b.c.D");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesBreadthFirstFolderSearch() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
|
||||
String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource());
|
||||
assertThat(actual).isEqualTo("a.B");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findSingleFolderSearch() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Unable to find a single main class "
|
||||
+ "from the following candidates [a.B, a.b.c.E]");
|
||||
MainClassFinder.findSingleMainClass(this.testJarFile.getJarSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findSingleFolderSearchPrefersAnnotatedMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", AnnotatedClassWithMainMethod.class);
|
||||
String mainClass = MainClassFinder.findSingleMainClass(
|
||||
this.testJarFile.getJarSource(),
|
||||
"org.springframework.boot.loader.tools.sample.SomeApplication");
|
||||
assertThat(mainClass).isEqualTo("a.b.c.E");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doWithFolderMainMethods() throws Exception {
|
||||
this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/G.class", ClassWithMainMethod.class);
|
||||
ClassNameCollector callback = new ClassNameCollector();
|
||||
MainClassFinder.doWithMainClasses(this.testJarFile.getJarSource(), callback);
|
||||
assertThat(callback.getClassNames().toString()).isEqualTo("[a.b.G, a.b.c.D]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doWithJarMainMethods() throws Exception {
|
||||
this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/G.class", ClassWithMainMethod.class);
|
||||
ClassNameCollector callback = new ClassNameCollector();
|
||||
MainClassFinder.doWithMainClasses(this.testJarFile.getJarFile(), null, callback);
|
||||
assertThat(callback.getClassNames().toString()).isEqualTo("[a.b.G, a.b.c.D]");
|
||||
}
|
||||
|
||||
private static class ClassNameCollector implements MainClassCallback<Object> {
|
||||
|
||||
private final List<String> classNames = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Object doWith(MainClass mainClass) {
|
||||
this.classNames.add(mainClass.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<String> getClassNames() {
|
||||
return this.classNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Calendar;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link Repackager}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RepackagerTests {
|
||||
|
||||
private static final Libraries NO_LIBRARIES = (callback) -> {
|
||||
};
|
||||
|
||||
private static final long JAN_1_1980;
|
||||
|
||||
private static final long JAN_1_1985;
|
||||
|
||||
static {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(1980, 0, 1, 0, 0, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
JAN_1_1980 = calendar.getTime().getTime();
|
||||
calendar.set(Calendar.YEAR, 1985);
|
||||
JAN_1_1985 = calendar.getTime().getTime();
|
||||
}
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private TestJarFile testJarFile;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
this.testJarFile = new TestJarFile(this.temporaryFolder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullSource() throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new Repackager(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingSource() throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new Repackager(new File("missing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directorySource() throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new Repackager(this.temporaryFolder.getRoot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void specificMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setMainClass("a.b.C");
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class"))
|
||||
.isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mainClassFromManifest() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
|
||||
manifest.getMainAttributes().putValue("Main-Class", "a.b.C");
|
||||
this.testJarFile.addManifest(manifest);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class"))
|
||||
.isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mainClassFound() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class"))
|
||||
.isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jarIsOnlyRepackagedOnce() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class"))
|
||||
.isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleMainClassFound() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/D.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Unable to find a single main class "
|
||||
+ "from the following candidates [a.b.C, a.b.D]");
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Unable to find main class");
|
||||
new Repackager(this.testJarFile.getFile()).repackage(NO_LIBRARIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMainClassAndLayoutIsNone() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setLayout(new Layouts.None());
|
||||
repackager.repackage(file, NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMainClassAndLayoutIsNoneWithNoMain() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setLayout(new Layouts.None());
|
||||
repackager.repackage(file, NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo(null);
|
||||
assertThat(hasLauncherClasses(file)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameSourceAndDestinationWithBackup() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
assertThat(new File(file.getParent(), file.getName() + ".original")).exists();
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameSourceAndDestinationWithoutBackup() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setBackupSource(false);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
assertThat(new File(file.getParent(), file.getName() + ".original"))
|
||||
.doesNotExist();
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void differentDestination() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = this.temporaryFolder.newFile("different.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
assertThat(new File(source.getParent(), source.getName() + ".original"))
|
||||
.doesNotExist();
|
||||
assertThat(hasLauncherClasses(source)).isFalse();
|
||||
assertThat(hasLauncherClasses(dest)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullDestination() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Invalid destination");
|
||||
repackager.repackage(null, NO_LIBRARIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void destinationIsDirectory() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Invalid destination");
|
||||
repackager.repackage(this.temporaryFolder.getRoot(), NO_LIBRARIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overwriteDestination() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
File dest = this.temporaryFolder.newFile("dest.jar");
|
||||
dest.createNewFile();
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
assertThat(hasLauncherClasses(dest)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullLibraries() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Libraries must not be null");
|
||||
repackager.repackage(file, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void libraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.temporaryFolder);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
final File libJarFile = libJar.getFile();
|
||||
final File libJarFileToUnpack = libJar.getFile();
|
||||
final File libNonJarFile = this.temporaryFolder.newFile();
|
||||
FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile);
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + libJarFileToUnpack.getName(),
|
||||
libJarFileToUnpack);
|
||||
File file = this.testJarFile.getFile();
|
||||
libJarFile.setLastModified(JAN_1_1980);
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFileToUnpack, LibraryScope.COMPILE, true));
|
||||
callback.library(new Library(libNonJarFile, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFile.getName())).isTrue();
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName()))
|
||||
.isTrue();
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libNonJarFile.getName())).isFalse();
|
||||
JarEntry entry = getEntry(file, "BOOT-INF/lib/" + libJarFile.getName());
|
||||
assertThat(entry.getTime()).isEqualTo(JAN_1_1985);
|
||||
entry = getEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName());
|
||||
assertThat(entry.getComment()).startsWith("UNPACK:");
|
||||
assertThat(entry.getComment().length()).isEqualTo(47);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void duplicateLibraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.temporaryFolder);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
final File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Duplicate library");
|
||||
repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile, LibraryScope.COMPILE, false));
|
||||
callback.library(new Library(libJarFile, LibraryScope.COMPILE, false));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customLayout() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.temporaryFolder);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
final File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Layout layout = mock(Layout.class);
|
||||
final LibraryScope scope = mock(LibraryScope.class);
|
||||
given(layout.getLauncherClassName()).willReturn("testLauncher");
|
||||
given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/");
|
||||
given(layout.getLibraryDestination(anyString(), eq(LibraryScope.COMPILE)))
|
||||
.willReturn("test-lib/");
|
||||
repackager.setLayout(layout);
|
||||
repackager.repackage(
|
||||
(callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue();
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib"))
|
||||
.isEqualTo("test-lib/");
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("testLauncher");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customLayoutNoBootLib() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.temporaryFolder);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
final File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Layout layout = mock(Layout.class);
|
||||
final LibraryScope scope = mock(LibraryScope.class);
|
||||
given(layout.getLauncherClassName()).willReturn("testLauncher");
|
||||
repackager.setLayout(layout);
|
||||
repackager.repackage(
|
||||
(callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib"))
|
||||
.isNull();
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("testLauncher");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void springBootVersion() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes())
|
||||
.containsKey(new Attributes.Name("Spring-Boot-Version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executableJarLayoutAttributes() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes())
|
||||
.containsEntry(new Attributes.Name("Spring-Boot-Lib"), "BOOT-INF/lib/");
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(
|
||||
new Attributes.Name("Spring-Boot-Classes"), "BOOT-INF/classes/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executableWarLayoutAttributes() throws Exception {
|
||||
this.testJarFile.addClass("WEB-INF/classes/a/b/C.class",
|
||||
ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile("war");
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes())
|
||||
.containsEntry(new Attributes.Name("Spring-Boot-Lib"), "WEB-INF/lib/");
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(
|
||||
new Attributes.Name("Spring-Boot-Classes"), "WEB-INF/classes/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullCustomLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Layout must not be null");
|
||||
repackager.setLayout(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dontRecompressZips() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.temporaryFolder);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("test/nested.jar", nestedFile);
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> callback
|
||||
.library(new Library(nestedFile, LibraryScope.COMPILE)));
|
||||
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
assertThat(
|
||||
jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/test/nested.jar").getMethod())
|
||||
.isEqualTo(ZipEntry.STORED);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addLauncherScript() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = this.temporaryFolder.newFile("dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
LaunchScript script = new MockLauncherScript("ABC");
|
||||
repackager.repackage(dest, NO_LIBRARIES, script);
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(dest);
|
||||
assertThat(new String(bytes)).startsWith("ABC");
|
||||
assertThat(hasLauncherClasses(source)).isFalse();
|
||||
assertThat(hasLauncherClasses(dest)).isTrue();
|
||||
try {
|
||||
assertThat(Files.getPosixFilePermissions(dest.toPath()))
|
||||
.contains(PosixFilePermission.OWNER_EXECUTE);
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
// Probably running the test on Windows
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unpackLibrariesTakePrecedenceOverExistingSourceEntries()
|
||||
throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.temporaryFolder);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
String name = "BOOT-INF/lib/" + nestedFile.getName();
|
||||
this.testJarFile.addFile(name, nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> callback
|
||||
.library(new Library(nestedFile, LibraryScope.COMPILE, true)));
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
assertThat(jarFile.getEntry(name).getComment()).startsWith("UNPACK:");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingSourceEntriesTakePrecedenceOverStandardLibraries()
|
||||
throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.temporaryFolder);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(),
|
||||
nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
long sourceLength = nestedFile.length();
|
||||
repackager.repackage((callback) -> {
|
||||
nestedFile.delete();
|
||||
File toZip = RepackagerTests.this.temporaryFolder.newFile();
|
||||
ZipUtil.packEntry(toZip, nestedFile);
|
||||
callback.library(new Library(nestedFile, LibraryScope.COMPILE));
|
||||
});
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize())
|
||||
.isEqualTo(sourceLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void metaInfIndexListIsRemovedFromRepackagedJar() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("META-INF/INDEX.LIST",
|
||||
this.temporaryFolder.newFile("INDEX.LIST"));
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = this.temporaryFolder.newFile("dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (JarFile jarFile = new JarFile(dest)) {
|
||||
assertThat(jarFile.getEntry("META-INF/INDEX.LIST")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customLayoutFactoryWithoutLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(source, new TestLayoutFactory());
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
JarFile jarFile = new JarFile(source);
|
||||
assertThat(jarFile.getEntry("test")).isNotNull();
|
||||
jarFile.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customLayoutFactoryWithLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(source, new TestLayoutFactory());
|
||||
repackager.setLayout(new Layouts.Jar());
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
JarFile jarFile = new JarFile(source);
|
||||
assertThat(jarFile.getEntry("test")).isNull();
|
||||
jarFile.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void metaInfAopXmlIsMovedBeneathBootInfClassesWhenRepackaged()
|
||||
throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("META-INF/aop.xml",
|
||||
this.temporaryFolder.newFile("aop.xml"));
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = this.temporaryFolder.newFile("dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (JarFile jarFile = new JarFile(dest)) {
|
||||
assertThat(jarFile.getEntry("META-INF/aop.xml")).isNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/aop.xml")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = this.temporaryFolder.newFile("dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (ZipFile zip = new ZipFile(dest)) {
|
||||
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
assertThat(entry.getPlatform()).isEqualTo(ZipArchiveEntry.PLATFORM_UNIX);
|
||||
assertThat(entry.getGeneralPurposeBit().usesUTF8ForNames()).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasLauncherClasses(File file) throws IOException {
|
||||
return hasEntry(file, "org/springframework/boot/")
|
||||
&& hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");
|
||||
}
|
||||
|
||||
private boolean hasEntry(File file, String name) throws IOException {
|
||||
return getEntry(file, name) != null;
|
||||
}
|
||||
|
||||
private JarEntry getEntry(File file, String name) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
return jarFile.getJarEntry(name);
|
||||
}
|
||||
}
|
||||
|
||||
private Manifest getManifest(File file) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
return jarFile.getManifest();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MockLauncherScript implements LaunchScript {
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
MockLauncherScript(String script) {
|
||||
this.bytes = script.getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
return this.bytes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TestLayoutFactory implements LayoutFactory {
|
||||
|
||||
@Override
|
||||
public Layout getLayout(File source) {
|
||||
return new TestLayout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestLayout extends Layouts.Jar implements CustomLoaderLayout {
|
||||
|
||||
@Override
|
||||
public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException {
|
||||
writer.writeEntry("test", new ByteArrayInputStream("test".getBytes()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
/**
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class TestJarFile {
|
||||
|
||||
private final byte[] buffer = new byte[4096];
|
||||
|
||||
private final TemporaryFolder temporaryFolder;
|
||||
|
||||
private final File jarSource;
|
||||
|
||||
public TestJarFile(TemporaryFolder temporaryFolder) throws IOException {
|
||||
this.temporaryFolder = temporaryFolder;
|
||||
this.jarSource = temporaryFolder.newFolder();
|
||||
}
|
||||
|
||||
public void addClass(String filename, Class<?> classToCopy) throws IOException {
|
||||
addClass(filename, classToCopy, null);
|
||||
}
|
||||
|
||||
public void addClass(String filename, Class<?> classToCopy, Long time)
|
||||
throws IOException {
|
||||
File file = getFilePath(filename);
|
||||
file.getParentFile().mkdirs();
|
||||
InputStream inputStream = getClass().getResourceAsStream(
|
||||
"/" + classToCopy.getName().replace('.', '/') + ".class");
|
||||
copyToFile(inputStream, file);
|
||||
if (time != null) {
|
||||
file.setLastModified(time);
|
||||
}
|
||||
}
|
||||
|
||||
public void addFile(String filename, File fileToCopy) throws IOException {
|
||||
File file = getFilePath(filename);
|
||||
file.getParentFile().mkdirs();
|
||||
try (InputStream inputStream = new FileInputStream(fileToCopy)) {
|
||||
copyToFile(inputStream, file);
|
||||
}
|
||||
}
|
||||
|
||||
public void addManifest(Manifest manifest) throws IOException {
|
||||
File manifestFile = new File(this.jarSource, "META-INF/MANIFEST.MF");
|
||||
manifestFile.getParentFile().mkdirs();
|
||||
try (OutputStream outputStream = new FileOutputStream(manifestFile)) {
|
||||
manifest.write(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private File getFilePath(String filename) {
|
||||
String[] paths = filename.split("\\/");
|
||||
File file = this.jarSource;
|
||||
for (String path : paths) {
|
||||
file = new File(file, path);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private void copyToFile(InputStream inputStream, File file)
|
||||
throws FileNotFoundException, IOException {
|
||||
try (OutputStream outputStream = new FileOutputStream(file)) {
|
||||
copy(inputStream, outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void copy(InputStream in, OutputStream out) throws IOException {
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = in.read(this.buffer)) != -1) {
|
||||
out.write(this.buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
public JarFile getJarFile() throws IOException {
|
||||
return new JarFile(getFile());
|
||||
}
|
||||
|
||||
public File getJarSource() {
|
||||
return this.jarSource;
|
||||
}
|
||||
|
||||
public File getFile() throws IOException {
|
||||
return getFile("jar");
|
||||
}
|
||||
|
||||
public File getFile(String extension) throws IOException {
|
||||
File file = this.temporaryFolder.newFile();
|
||||
file = new File(file.getParent(), file.getName() + "." + extension);
|
||||
ZipUtil.pack(this.jarSource, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools.sample;
|
||||
|
||||
/**
|
||||
* Sample annotated class with a main method.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@SomeApplication
|
||||
public class AnnotatedClassWithMainMethod {
|
||||
|
||||
public void run() {
|
||||
System.out.println("Hello World");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new AnnotatedClassWithMainMethod().run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools.sample;
|
||||
|
||||
/**
|
||||
* Sample class with a main method.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class ClassWithMainMethod {
|
||||
|
||||
public void run() {
|
||||
System.out.println("Hello World");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new ClassWithMainMethod().run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools.sample;
|
||||
|
||||
/**
|
||||
* Sample class without a main method.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class ClassWithoutMainMethod {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* 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.springframework.boot.loader.tools.sample;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Test annotation for a main application class.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface SomeApplication {
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
FOO=BAR
|
||||
Reference in New Issue
Block a user