001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.common.utils.batch;
024
025import java.io.File;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.ObjectInputStream;
029import java.io.ObjectOutputStream;
030import java.io.OutputStream;
031import java.lang.reflect.Constructor;
032import java.lang.reflect.InvocationTargetException;
033import java.util.List;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import dk.netarkivet.common.exceptions.ArgumentNotValid;
039import dk.netarkivet.common.exceptions.IOFailure;
040
041/**
042 * This implementation of FileBatchJob is a bridge to a jar file given as a File object. The given class will be loaded
043 * and used to perform the actions of the FileBatchJob class.
044 */
045@SuppressWarnings({"unchecked", "rawtypes", "serial"})
046public class LoadableJarBatchJob extends FileBatchJob {
047
048    /** The log. */
049    private static final transient Logger log = LoggerFactory.getLogger(LoadableJarBatchJob.class);
050
051    /** The FileBatchJob that this LoadableJarBatchJob is a wrapper for. */
052    transient FileBatchJob loadedJob;
053
054    /** The ClassLoader of type ByteJarLoader associated with this job. */
055    private ClassLoader multipleClassLoader;
056
057    /** The name of the loaded Job. */
058    private String jobClass;
059
060    /** The arguments for instantiating the batchjob. */
061    private List<String> args;
062
063    /**
064     * Load a given class from a jar file.
065     *
066     * @param jarFiles The jar file(s) to load from. This file may also contain other classes required by the
067     * FileBatchJob class.
068     * @param arguments The arguments for the batchjob.
069     * @param jobClass The class to load initially. This must be a subclass of FileBatchJob.
070     * @throws ArgumentNotValid If any of the arguments are null.
071     */
072    public LoadableJarBatchJob(String jobClass, List<String> arguments, File... jarFiles) throws ArgumentNotValid {
073        ArgumentNotValid.checkNotNull(jarFiles, "File jarFile");
074        ArgumentNotValid.checkNotNullOrEmpty(jobClass, "String jobClass");
075        ArgumentNotValid.checkNotNull(arguments, "List<String> arguments");
076        this.jobClass = jobClass;
077        this.args = arguments;
078        StringBuffer res = new StringBuffer("Loading loadableJarBatchJob using jarfiles: ");
079        for (File jarFile : jarFiles) {
080            res.append(jarFile.getName());
081        }
082        res.append(" and jobclass '" + jobClass);
083        if (!args.isEmpty()) {
084            res.append(", and arguments: '" + args + "'.");
085        }
086        log.info(res.toString());
087        multipleClassLoader = new ByteJarLoader(jarFiles);
088
089        // Ensure that the batchjob can be loaded.
090        loadBatchJob();
091    }
092
093    /**
094     * Method for initialising the batch job.
095     *
096     * @throws IOFailure If the job is not loaded correctly.
097     */
098    private void loadBatchJob() throws IOFailure {
099        try {
100            Class batchClass = multipleClassLoader.loadClass(jobClass);
101
102            if (args.size() == 0) {
103                // just load if no arguments.
104                loadedJob = (FileBatchJob) batchClass.newInstance();
105            } else {
106                // get argument classes (string only).
107                Class[] argClasses = new Class[args.size()];
108                for (int i = 0; i < args.size(); i++) {
109                    argClasses[i] = String.class;
110                }
111
112                // extract the constructor and instantiate the batchjob.
113                Constructor con = batchClass.getConstructor(argClasses);
114                loadedJob = (FileBatchJob) con.newInstance(args.toArray());
115                log.debug("Loaded batchjob with arguments: '{}'.", args);
116            }
117        } catch (InvocationTargetException e) {
118            final String msg = "Not allowed to invoke the batchjob '" + jobClass + "'.";
119            log.warn(msg, e);
120            throw new IOFailure(msg, e);
121        } catch (NoSuchMethodException e) {
122            final String msg = "No constructor for the arguments '" + args + "' can be found for the batchjob '"
123                    + jobClass + "'.";
124            log.warn(msg, e);
125            throw new IOFailure(msg, e);
126        } catch (InstantiationException e) {
127            final String msg = "Cannot instantiate loaded job class";
128            log.warn(msg, e);
129            throw new IOFailure(msg, e);
130        } catch (IllegalAccessException e) {
131            final String msg = "Cannot access loaded job from byte array";
132            log.warn(msg, e);
133            throw new IOFailure(msg, e);
134        } catch (ClassNotFoundException e) {
135            final String msg = "Cannot create job class from jar file";
136            log.warn(msg, e);
137            throw new IOFailure(msg, e);
138        }
139    }
140
141    /**
142     * Initialize the job before running. This is called before the processFile() calls.
143     *
144     * @param os the OutputStream to which output should be written
145     */
146    public void initialize(OutputStream os) {
147        ArgumentNotValid.checkNotNull(os, "os");
148
149        // Initialise the loadedJob.
150        loadBatchJob();
151        loadedJob.initialize(os);
152    }
153
154    /**
155     * Process one file stored in the bit archive.
156     *
157     * @param file the file to be processed.
158     * @param os the OutputStream to which output should be written
159     * @return true if the file was successfully processed, false otherwise
160     */
161    public boolean processFile(File file, OutputStream os) {
162        ArgumentNotValid.checkNotNull(file, "File file");
163        ArgumentNotValid.checkNotNull(os, "OutputStream os");
164        return loadedJob.processFile(file, os);
165    }
166
167    /**
168     * Finish the job. This is called after the last process() call.
169     *
170     * @param os the OutputStream to which output should be written
171     */
172    public void finish(OutputStream os) {
173        ArgumentNotValid.checkNotNull(os, "OutputStream os");
174        loadedJob.finish(os);
175    }
176
177    /**
178     * Human readable representation of this object. Overrides FileBatchJob.toString to include name of loaded
179     * jar/class.
180     *
181     * @return a Human readable representation of this class
182     */
183    public String toString() {
184        return this.getClass().getName() + " processing " + jobClass + " from " + multipleClassLoader.toString();
185    }
186
187    /**
188     * Override of the default way to serialize this class.
189     *
190     * @param out Stream that the object will be written to.
191     * @throws IOException In case there is an error from the underlying stream, or this object cannot be serialized.
192     */
193    private void writeObject(ObjectOutputStream out) throws IOException {
194        out.defaultWriteObject();
195    }
196
197    /**
198     * Override of the default way to deserialize an object of this class.
199     *
200     * @param in Stream that the object can be read from.
201     * @throws IOException If there is an error reading from the stream, or the serialized object cannot be deserialized
202     * due to errors in the serialized form.
203     * @throws ClassNotFoundException If the class definition of the serialized object cannot be found.
204     */
205    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
206        in.defaultReadObject();
207    }
208
209    @Override
210    public boolean postProcess(InputStream input, OutputStream output) {
211        ArgumentNotValid.checkNotNull(input, "InputStream input");
212        ArgumentNotValid.checkNotNull(output, "OutputStream output");
213
214        // Let the loaded job handle the post processing.
215        log.debug("Post-processing in the loaded batchjob.");
216        loadBatchJob();
217        return loadedJob.postProcess(input, output);
218    }
219
220    /**
221     * Method for retrieving the name of the loaded class.
222     *
223     * @return The name of the loaded class.
224     */
225    public String getLoadedJobClass() {
226        return jobClass;
227    }
228
229}