Java tutorial
/* * 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"); } }