/*
* Copyright (C) 2008 Yohan Liyanage.
*
* 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.nebulaframework.core.job.archive;
import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nebulaframework.core.job.GridJob;
import org.nebulaframework.deployment.classloading.GridArchiveClassLoader;
import org.nebulaframework.util.hashing.SHA1Generator;
import org.nebulaframework.util.io.IOSupport;
import org.springframework.util.Assert;
/**
* Represents an archived {@code GridJob}. An archived {@code GridJob} is a
* special format of {@code JAR}, which is referred to as
* {@code Nebula Archive}, identified by the extension {@code .nar}.
* <p>
* Nebula Archives allow required libraries ({@code .jar}) to be included
* within the archive itself, unlike the standard {@code JAR} file format.
* <p>
* The structure of Nebula Archive is as follows: <code>
* <pre>
* META-INF/
* |
* - Manifest.mf
* - ...
* NEBULA-INF/
* |
* - lib /
* |
* - library1.jar
* - ...
* - ...
* yourpackage /
* |
* - ...
* your.class
* ...
* </pre>
* </code> The libraries are to be packaged in {@code NEBULA-INF/lib} directory.
* A special class loader is used by Nebula Framework to load required classes
* from the libraries included in a Nebula Archive. Refer to
* {@link GridArchiveClassLoader} for additional information regarding class
* loading.
* <p>
* {@code GridArchive} keeps the {@code byte[]} of a {@code .nar} file, and SHA1
* Hash for the {@code byte[]}, for verification purposes. The hash for the
* {@code byte[]} is generated at the time of creation of the
* {@code GridArchive} instance for a {@code .nar} file. At each remote node,
* this hash will be compared with a SHA1-Hash generated at the time, to ensure
* that the {@code GridArchive} contains valid data.
* <p>
* To instantiate a {@code GridArchive}, use the following factory methods
* <ul>
* <li>{@link #fromFile(File)}</li>
* </ul>
* <p>
* This class implements {@link Externalizable} interface, instead of
* {@link Serializable} to improve performance in communications, by reducing
* the data transfer amount and serialization time [Grosso, W. 2001. "Java RMI",
* Section 10.7.1].
*
* @author Yohan Liyanage
* @version 1.0
*
* @see GridJob
* @see GridArchiveClassLoader
*/
public class GridArchive implements Serializable {
private static final long serialVersionUID = -5657326124238562531L;
private static Log log = LogFactory.getLog(GridArchive.class);
/**
* Default directory name for {@code NEBULA-INF} inside a Nebula Archive
* file.
*/
public static final String NEBULA_INF = "NEBULA-INF";
/**
* Default path for JAR Libraries inside a Nebula Archive file.
*/
public static final String LIBRARY_PATH = NEBULA_INF + "/lib";
private String[] jobClassNames; // Class Names of GridJobs in .nar
private byte[] bytes; // bytes of .nar file
private String hash; // SHA1 Hash for bytes
/**
* Constructs a {@code GridArchive} with given bytes of {@code .nar} file,
* and the names of {@code GridJob} classes.
* <p>
* SHA-1 Hash for the given {@code byte[]} will be calculated during the
* instantiation process.
* <p>
* Note that the constructor is of <b>{@code protected}</b> scope. To
* instantiate this type, use the factory method {@link #fromFile(File)}.
*
* @param bytes
* {@code byte[]} of {@code .nar} file
* @param jobClassNames
* {@code String[]} of fully qualified class names of
* {@code GridJob} classes inside the .nar file.
*
* @see #fromFile(File)
*/
protected GridArchive(byte[] bytes, String[] jobClassNames) {
super();
// Assertions
Assert.notNull(bytes);
Assert.notNull(jobClassNames);
this.bytes = bytes;
this.jobClassNames = jobClassNames;
// Generate SHA1 Hash for bytes
hash = SHA1Generator.generateAsString(bytes);
}
/**
* Returns the bytes of the {@code .nar} file, represented by this
* {@code GridArchive} instance.
* <p>
* This is a clone of internal byte[]. Changes to the return value will not
* be reflected by this GridArchive.
*
* @return bytes of {@code .nar} file
*/
public byte[] getBytes() {
// Return a clone to protect internal state
return bytes.clone();
}
/**
* Returns the SHA-1 Hash generated at the time of creation of this
* {@code GridArchive}, for the bytes of {@code .nar} file.
*
* @return SHA-1 Hash as {@code String}
*/
public String getHash() {
return hash;
}
/**
* Returns an array of {@code String}s, which contains fully qualified
* class names of {@code GridJob}s inside the {@code .nar} file.
* <p>
* Note that this method returns a clone of the intenal object, and any
* changes to the return value will not be reflected in this GridArchive
* instance.
*
* @return Class names of {@code GridJob}s inside {@code .nar} file
*/
public String[] getJobClassNames() {
return jobClassNames.clone();
}
/**
* <b>Factory Method</b> to create a {@code GridArchive} instance for the
* given {@code File} instance of a {@code .nar} file.
*
* @param file
* {@code File} instance of {@code .nar} file.
*
* @return {@code GridArchive} instance for given {@code .nar} file.
*
* @throws GridArchiveException
* if processing of {@code File} failed.
*/
public static GridArchive fromFile(File file) throws GridArchiveException {
try {
// Assertions
Assert.notNull(file);
// Verify file integrity
if (!verify(file)) {
throw new SecurityException(
"Grid Archive Verification failed of " + file);
}
// Detect the GridJob Class names
String[] jobClassNames = findJobClassNames(file);
// Read byte[] from File
byte[] bytes = IOSupport.readBytes(new FileInputStream(file));
// Create and return GridArchive
return new GridArchive(bytes, jobClassNames);
} catch (Exception e) {
throw new GridArchiveException("Cannot create Grid Archive", e);
}
}
/**
* Verifies the integrity of the given {@code File}, as a Nebula Archive.
*
* @param file
* {@code File} to be verified.
* @return if success, {@code true}, otherwise {@code false}.
*/
protected static boolean verify(File file) {
// FIXME Implement to verify the NAR
return file.exists();
}
/**
* Returns the {@code GridJob} classes with in the given {@code .nar} file.
* Uses {@link GridArchiveClassLoader}.
*
* @param file
* {@code File} instance for {@code .nar} file.
*
* @return Fully qualified class names of {@code GridJob} classes in the
* file.
*
* @throws IOException
* if occurred during File I/O operations
*
* @see GridArchiveClassLoader
*/
protected static String[] findJobClassNames(final File file)
throws IOException {
// Instantiate ClassLoader for given File
ClassLoader classLoader = AccessController
.doPrivileged(new PrivilegedAction<ClassLoader>() {
public GridArchiveClassLoader run() {
return new GridArchiveClassLoader(file);
}
});
// Find ClassNames of all classes inside the file (except in NEBULA-INF)
// Content inside .jar files will not be processed
String[] allClassNames = getAllClassNames(file);
// Holds Class<?> instances loaded by ClassLoader, for all classes
List<String> jobClassNames = new ArrayList<String>();
for (String className : allClassNames) {
try {
// Load each Class and check if its a GridJob Class
if (isGridJobClass(classLoader.loadClass(className))) {
jobClassNames.add(className);
}
} catch (ClassNotFoundException e) {
// Log and continue with rest
log.debug("[GridArchive] Unable to load class " + className);
}
}
return jobClassNames.toArray(new String[] {});
}
/**
* Detects all classes inside the given {@code .nar} file and returns an
* array of fully qualified class name of each class, as {@code String}.
*
* @param file
* {@code .nar File}
*
* @return Fully qualified class names classes in {@code File}
*
* @throws IOException
* if occurred during File I/O operations
*/
protected static String[] getAllClassNames(File file) throws IOException {
// Holds Class Names
List<String> names = new ArrayList<String>();
// Create ZipArchive for File
ZipFile archive = new ZipFile(file);
Enumeration<? extends ZipEntry> entries = archive.entries();
// Read each entry in archive
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
// Ignore Directories
if (entry.isDirectory())
continue;
// Ignore content in NEBULA-INF
if (entry.getName().startsWith(GridArchive.NEBULA_INF)) {
continue;
}
// Add each file which is a valid class file to list
if (isClass(entry.getName())) {
names.add(toClassName(entry.getName()));
}
}
return names.toArray(new String[] {});
}
/**
* Detects whether a given {@code Class} implements
* {@code GridJob interface}, using Reflection API.
*
* @param cls
* {@code Class} to be checked
* @return if {@code GridJob} class, {@code true}, otherwise {@code false}
*/
protected static boolean isGridJobClass(Class<?> cls) {
// Get all interfaces, and process each
for (Class<?> iface : cls.getInterfaces()) {
// If class implements GridJob interfaces
if (isGridJobInterface(iface)) {
log.debug("[GridArchive] Found GridJob Class " + cls.getName());
return true;
}
}
return false;
}
/**
* Returns true if the given interface is a sub-interface of {@code GridJob}
* marker interface.
*
* @param intrface
* interface to check
* @return if {@code GridJob}, {@code true}, otherwise {@code false}
*/
private static boolean isGridJobInterface(Class<?> intrface) {
for (Class<?> iface : intrface.getInterfaces()) {
if (iface.getName().equals(GridJob.class.getName())) {
return true;
}
}
return false;
}
/**
* Converts the given file name to fully qualified class name. For example,
* for '{@code org/nebulaframework/Grid.class}', this method returns '{@code org.nebulaframework.Grid}'.
*
* @param fileName
* File name to be converted
* @return Fully qualified Class Name
*/
protected static String toClassName(String fileName) {
String name = fileName.substring(0, fileName.length()
- ".class".length());
return name.replaceAll("\\/|\\\\", "."); // Replace all path
// separators (Win/Linux)
}
/**
* Returns {@code true} if the given file name (path) identifies a class
* file. The identification is done by checking if the file name ends with '{@code .class}'.
*
* @param fileName
* File Name to be checked
* @return if class, {@code true}, otherwise {@code false}
*/
protected static boolean isClass(String fileName) {
return fileName.endsWith(".class");
}
}