/**
* Copyright 2011-2014 Asakusa Framework Team.
*
* 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 com.asakusafw.runtime.configuration;
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.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xerial.snappy.Snappy;
import com.asakusafw.runtime.stage.launcher.ApplicationLauncher;
/**
* Deploys mock framework environment.
*/
public class FrameworkDeployer implements TestRule {
static final Logger LOG = LoggerFactory.getLogger(FrameworkDeployer.class);
final TemporaryFolder folder = new TemporaryFolder();
final boolean copyDefaults;
private File home;
private File work;
private File bootstrapJar;
private final List<File> runtimeJars = new ArrayList<File>();
/**
* Creates a new instance with creating default layout.
*/
public FrameworkDeployer() {
this(true);
}
/**
* Creates a new instance.
* @param copyDefaults creates default layout
*/
public FrameworkDeployer(boolean copyDefaults) {
this.copyDefaults = copyDefaults;
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
folder.create();
try {
createDirs();
if (copyDefaults) {
deployMainDist();
deployTestDist();
deployRuntimeLibrary();
}
deploy();
base.evaluate();
} finally {
folder.delete();
}
}
};
}
void createDirs() throws IOException {
home = folder.newFolder("home");
work = folder.newFolder("work");
}
/**
* Deploys project-specific configurations.
* @throws Throwable if failed to deploy
*/
protected void deploy() throws Throwable {
return;
}
void deployMainDist() throws IOException {
LOG.debug("Deploying src/main/dist");
File source = new File("src/main/dist");
if (source.exists()) {
copy(source, getHome());
} else {
LOG.debug("There is no prod distribution directory: {}", source.getAbsolutePath());
}
}
void deployTestDist() throws IOException {
LOG.debug("Deploying src/test/dist");
File source = new File("src/test/dist");
if (source.exists()) {
copy(source, getHome());
} else {
LOG.debug("There is no test distribution directory: {}", source.getAbsolutePath());
}
}
void deployRuntimeLibrary() throws IOException {
LOG.debug("Deploying runtime library");
bootstrapJar = deployFatLibrary("core/lib/asakusa-runtime-all.jar",
ApplicationLauncher.class,
Snappy.class);
}
private File deployFatLibrary(String targetPath, Class<?>... classes) throws IOException {
if (classes.length == 1) {
return deployLibrary(classes[0], targetPath);
}
List<File> paths = new ArrayList<File>();
for (Class<?> aClass : classes) {
File path = findLibraryPathFromClass(aClass);
if (path == null) {
throw new IOException(MessageFormat.format(
"Failed to detect library archive for \"{0}\"",
aClass.getName()));
}
paths.add(path);
}
File target = new File(getHome(), targetPath);
deployFatLibrary(paths, target);
return target;
}
private void deployFatLibrary(List<File> paths, File target) throws IOException {
prepareParent(target);
Set<String> saw = new HashSet<String>();
ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(target));
try {
for (File path : paths) {
if (path.isDirectory()) {
putEntry(zip, path, null, saw);
} else {
mergeEntries(zip, path, saw);
}
}
} finally {
zip.close();
}
}
private void mergeEntries(ZipOutputStream zip, File file, Set<String> saw) throws IOException {
ZipInputStream in = new ZipInputStream(new FileInputStream(file));
try {
while (true) {
ZipEntry entry = in.getNextEntry();
if (entry == null) {
break;
}
if (saw.contains(entry.getName())) {
continue;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Copy into archive: {} -> {}",
entry.getName(),
file);
}
saw.add(entry.getName());
zip.putNextEntry(new ZipEntry(entry.getName()));
copyStream(in, zip);
}
} finally {
in.close();
}
}
/**
* Deploys class library archive into target path.
* @param memberClass a member class which the target library includes in
* @param targetPath target relative path from {@link #getHome()}
* @return deployed file
* @throws IOException if failed to deploy
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public File deployLibrary(Class<?> memberClass, String targetPath) throws IOException {
File archive = findLibraryPathFromClass(memberClass);
if (archive == null) {
throw new IOException(MessageFormat.format(
"Failed to detect library archive for \"{0}\"",
targetPath));
}
File target = new File(getHome(), targetPath);
deployLibrary(archive, target);
return target;
}
/**
* Find library file/directory from an element class.
* @param aClass element class in target library
* @return target file/directory, or {@code null} if not found
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public File findLibraryPathFromClass(Class<?> aClass) {
if (aClass == null) {
throw new IllegalArgumentException("aClass must not be null"); //$NON-NLS-1$
}
int start = aClass.getName().lastIndexOf('.') + 1;
String name = aClass.getName().substring(start);
URL resource = aClass.getResource(name + ".class");
if (resource == null) {
LOG.warn("Failed to locate the class file: {}", aClass.getName());
return null;
}
String protocol = resource.getProtocol();
if (protocol.equals("file")) {
try {
File file = new File(resource.toURI());
return toClassPathRoot(aClass, file);
} catch (URISyntaxException e) {
LOG.warn(MessageFormat.format(
"Failed to locate the library path (cannot convert to local file): {0}",
resource), e);
return null;
}
}
if (protocol.equals("jar")) {
String path = resource.getPath();
return toClassPathRoot(aClass, path);
} else {
LOG.warn("Failed to locate the library path (unsupported protocol {}): {}",
resource,
aClass.getName());
return null;
}
}
private File toClassPathRoot(Class<?> aClass, File classFile) {
assert aClass != null;
assert classFile != null;
assert classFile.isFile();
String name = aClass.getName();
File current = classFile.getParentFile();
assert current != null && current.isDirectory() : classFile;
for (int i = name.indexOf('.'); i >= 0; i = name.indexOf('.', i + 1)) {
current = current.getParentFile();
assert current != null && current.isDirectory() : classFile;
}
return current;
}
private File toClassPathRoot(Class<?> aClass, String uriQualifiedPath) {
assert aClass != null;
assert uriQualifiedPath != null;
int entry = uriQualifiedPath.lastIndexOf('!');
String qualifier;
if (entry >= 0) {
qualifier = uriQualifiedPath.substring(0, entry);
} else {
qualifier = uriQualifiedPath;
}
URI archive;
try {
archive = new URI(qualifier);
} catch (URISyntaxException e) {
LOG.warn(MessageFormat.format(
"Failed to locate the JAR library file {}: {}",
qualifier,
aClass.getName()),
e);
throw new UnsupportedOperationException(qualifier, e);
}
if (archive.getScheme().equals("file") == false) {
LOG.warn("Failed to locate the library path (unsupported protocol {}): {}",
archive,
aClass.getName());
return null;
}
File file = new File(archive);
assert file.isFile() : file;
return file;
}
/**
* Deploys source archive/directory onto the target jar file.
* @param source source archive/directory
* @param target target jar file
* @return deployed
* @throws IOException if failed to put
* @throws IllegalArgumentException if some parameters were {@code null}
* @see #deployLibrary(Class, String)
*/
public File deployLibrary(File source, File target) throws IOException {
if (source == null) {
throw new IllegalArgumentException("source must not be null"); //$NON-NLS-1$
}
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
if (source.isFile()) {
copy(source, target);
} else {
LOG.debug("Package into archive: {} -> {}", source, target);
prepareParent(target);
OutputStream output = new FileOutputStream(target);
try {
ZipOutputStream zip = new ZipOutputStream(output);
putEntry(zip, source, null, new HashSet<String>());
zip.close();
} finally {
output.close();
}
}
return target;
}
private void putEntry(
ZipOutputStream zip,
File source, String path,
Set<String> saw) throws IOException {
assert zip != null;
assert source != null;
assert !(source.isFile() && path == null);
if (source.isDirectory()) {
for (File child : source.listFiles()) {
String next = (path == null) ? child.getName() : path + '/' + child.getName();
putEntry(zip, child, next, saw);
}
} else {
if (saw.contains(path)) {
return;
}
saw.add(path);
zip.putNextEntry(new ZipEntry(path));
InputStream in = new FileInputStream(source);
try {
LOG.debug("Copy into archive: {} -> {}", source, path);
copyStream(in, zip);
} finally {
in.close();
}
zip.closeEntry();
}
}
/**
* Copies a file/directory into target.
* @param source source file/directory
* @param target target path
* @return same as {@code target}
* @throws IOException if failed to copy
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public File copy(File source, File target) throws IOException {
if (source == null) {
throw new IllegalArgumentException("source must not be null"); //$NON-NLS-1$
}
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
if (source.isDirectory()) {
for (File child : source.listFiles()) {
copy(child, new File(target, child.getName()));
}
} else {
copyFile(source, target);
}
return target;
}
/**
* Copies an input stram into target file.
* @param input input stream
* @param target target path
* @return same as {@code target}
* @throws IOException if failed to copy
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public File dump(InputStream input, File target) throws IOException {
if (input == null) {
throw new IllegalArgumentException("input must not be null"); //$NON-NLS-1$
}
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
prepareParent(target);
OutputStream output = new FileOutputStream(target);
try {
copyStream(input, output);
} finally {
output.close();
}
return target;
}
private void copyFile(File source, File target) throws FileNotFoundException, IOException {
assert source != null;
assert target != null;
InputStream input = new FileInputStream(source);
try {
prepareParent(target);
OutputStream output = new FileOutputStream(target);
try {
copyStream(input, output);
} finally {
output.close();
}
} finally {
input.close();
}
if (source.canExecute()) {
target.setExecutable(true);
}
}
private void copyStream(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[512];
while (true) {
int read = input.read(buf);
if (read < 0) {
break;
}
output.write(buf, 0, read);
}
}
private void prepareParent(File target) throws IOException {
assert target != null;
if (target.getParentFile().isDirectory() == false && target.getParentFile().mkdirs() == false) {
throw new IOException(MessageFormat.format(
"Failed to copy into {0} (cannot create target directory)",
target));
}
}
/**
* Extracts ZIP archive into target directory.
* @param archive ZIP archive
* @param target target directory
* @return same as {@code target}
* @throws IOException if failed
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public File extract(File archive, File target) throws IOException {
if (archive == null) {
throw new IllegalArgumentException("archive must not be null"); //$NON-NLS-1$
}
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
InputStream input = new FileInputStream(target);
try {
ZipInputStream zip = new ZipInputStream(input);
extract(zip, target);
} finally {
input.close();
}
return target;
}
/**
* Extracts ZIP archive into target directory.
* @param input ZIP archive
* @param target target directory
* @return same as {@code target}
* @throws IOException if failed to extract archive into target
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public File extract(ZipInputStream input, File target) throws IOException {
if (input == null) {
throw new IllegalArgumentException("input must not be null"); //$NON-NLS-1$
}
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
while (true) {
ZipEntry entry = input.getNextEntry();
if (entry == null) {
break;
}
if (entry.isDirectory()) {
continue;
}
File file = new File(target, entry.getName());
dump(input, file);
}
return target;
}
/**
* Returns path to framework deployed.
* @return framework path
*/
public File getHome() {
return home;
}
/**
* Returns path to temporary work directory.
* @param child the child name
* @return work directory
*/
public File getWork(String child) {
File dir = new File(work, child);
if (dir.mkdirs() == false && dir.isDirectory() == false) {
LOG.warn("Failed to create directory: {}", dir.getAbsolutePath());
}
return dir;
}
/**
* Returns the path to the core runtime library.
* @return the path, or {@code null} if not put
*/
public File getCoreRuntimeLibrary() {
return bootstrapJar;
}
/**
* Returns the paths to the runtime libraries (except the core library).
* @return the paths
*/
public List<File> getRuntimeLibraries() {
return Collections.unmodifiableList(runtimeJars);
}
/**
* Returns the path to the core configuration file.
* @return the path, or {@code null} if not put
*/
public File getCoreConfigurationFile() {
File conf = new File(getHome(), "core/conf/asakusa-resources.xml");
if (conf.exists()) {
return conf;
}
return null;
}
}