Java tutorial
/* */ package org.taverna.server.master.worker; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ import static java.lang.System.currentTimeMillis; import static java.util.Calendar.MINUTE; import static java.util.Collections.sort; import static java.util.Collections.unmodifiableSet; import static java.util.UUID.randomUUID; import static org.apache.commons.io.IOUtils.closeQuietly; import static org.apache.commons.logging.LogFactory.getLog; import static org.taverna.server.master.worker.RemoteRunDelegate.checkBadFilename; import static org.taverna.server.master.worker.RunConnection.NAME_LENGTH; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.PipedOutputStream; import java.rmi.MarshalledObject; import java.rmi.RemoteException; import java.security.GeneralSecurityException; import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.annotation.Nonnull; import org.apache.commons.logging.Log; import org.taverna.server.localworker.remote.IllegalStateTransitionException; import org.taverna.server.localworker.remote.ImplementationException; import org.taverna.server.localworker.remote.RemoteDirectory; import org.taverna.server.localworker.remote.RemoteDirectoryEntry; import org.taverna.server.localworker.remote.RemoteFile; import org.taverna.server.localworker.remote.RemoteInput; import org.taverna.server.localworker.remote.RemoteListener; import org.taverna.server.localworker.remote.RemoteSingleRun; import org.taverna.server.localworker.remote.RemoteStatus; import org.taverna.server.localworker.remote.StillWorkingOnItException; import org.taverna.server.master.common.Status; import org.taverna.server.master.common.Workflow; import org.taverna.server.master.exceptions.BadPropertyValueException; import org.taverna.server.master.exceptions.BadStateChangeException; import org.taverna.server.master.exceptions.FilesystemAccessException; import org.taverna.server.master.exceptions.NoListenerException; import org.taverna.server.master.exceptions.OverloadedException; import org.taverna.server.master.exceptions.UnknownRunException; import org.taverna.server.master.interfaces.Directory; import org.taverna.server.master.interfaces.DirectoryEntry; import org.taverna.server.master.interfaces.File; import org.taverna.server.master.interfaces.Input; import org.taverna.server.master.interfaces.Listener; import org.taverna.server.master.interfaces.SecurityContextFactory; import org.taverna.server.master.interfaces.TavernaRun; import org.taverna.server.master.interfaces.TavernaSecurityContext; import org.taverna.server.master.utils.UsernamePrincipal; /** * Bridging shim between the WebApp world and the RMI world. * * @author Donal Fellows */ @SuppressWarnings("serial") public class RemoteRunDelegate implements TavernaRun { private transient Log log = getLog("Taverna.Server.Worker"); transient TavernaSecurityContext secContext; Date creationInstant; Workflow workflow; Date expiry; HashSet<String> readers; HashSet<String> writers; HashSet<String> destroyers; transient String id; transient RemoteSingleRun run; transient RunDBSupport db; transient FactoryBean factory; boolean doneTransitionToFinished; boolean generateProvenance;// FIXME expose String name; private static final String ELLIPSIS = "..."; public RemoteRunDelegate(Date creationInstant, Workflow workflow, RemoteSingleRun rsr, int defaultLifetime, RunDBSupport db, UUID id, boolean generateProvenance, FactoryBean factory) { if (rsr == null) throw new IllegalArgumentException("remote run must not be null"); this.creationInstant = creationInstant; this.workflow = workflow; Calendar c = Calendar.getInstance(); c.add(MINUTE, defaultLifetime); this.expiry = c.getTime(); this.run = rsr; this.db = db; this.generateProvenance = generateProvenance; this.factory = factory; try { this.name = ""; String ci = " " + creationInstant; String n = workflow.getName(); if (n.length() > NAME_LENGTH - ci.length()) n = n.substring(0, NAME_LENGTH - ci.length() - ELLIPSIS.length()) + ELLIPSIS; this.name = n + ci; } catch (Exception e) { // Ignore; it's just a name, not something important. } if (id != null) this.id = id.toString(); } RemoteRunDelegate() { } /** * Get the types of listener supported by this run. * * @return A list of listener type names. * @throws RemoteException * If anything goes wrong. */ public List<String> getListenerTypes() throws RemoteException { return run.getListenerTypes(); } @Override public void addListener(Listener listener) { if (listener instanceof ListenerDelegate) try { run.addListener(((ListenerDelegate) listener).getRemote()); } catch (RemoteException e) { log.warn("communication problem adding listener", e); } catch (ImplementationException e) { log.warn("implementation problem adding listener", e); } else log.fatal("bad listener " + listener.getClass() + "; not applicable remotely!"); } @Override public String getId() { if (id == null) id = randomUUID().toString(); return id; } /** * Attach a listener to a workflow run and return its local delegate. * * @param type * The type of listener to create. * @param config * The configuration of the listener. * @return The local delegate of the listener. * @throws NoListenerException * If anything goes wrong. */ public Listener makeListener(String type, String config) throws NoListenerException { try { return new ListenerDelegate(run.makeListener(type, config)); } catch (RemoteException e) { throw new NoListenerException("failed to make listener", e); } } @Override public void destroy() { try { run.destroy(); } catch (RemoteException | ImplementationException e) { log.warn("failed to destroy run", e); } } @Override public Date getExpiry() { return new Date(expiry.getTime()); } @Override public List<Listener> getListeners() { List<Listener> listeners = new ArrayList<>(); try { for (RemoteListener rl : run.getListeners()) listeners.add(new ListenerDelegate(rl)); } catch (RemoteException e) { log.warn("failed to get listeners", e); } return listeners; } @Override public TavernaSecurityContext getSecurityContext() { return secContext; } @Override public Status getStatus() { try { switch (run.getStatus()) { case Initialized: return Status.Initialized; case Operating: return Status.Operating; case Stopped: return Status.Stopped; case Finished: return Status.Finished; } } catch (RemoteException e) { log.warn("problem getting remote status", e); } return Status.Finished; } @Override public Workflow getWorkflow() { return workflow; } @Override public Directory getWorkingDirectory() throws FilesystemAccessException { try { return new DirectoryDelegate(run.getWorkingDirectory()); } catch (Throwable e) { if (e.getCause() != null) e = e.getCause(); throw new FilesystemAccessException("problem getting main working directory handle", e); } } @Override public void setExpiry(Date d) { if (d.after(new Date())) expiry = new Date(d.getTime()); db.flushToDisk(this); } @Override public String setStatus(Status s) throws BadStateChangeException { try { log.info("setting status of run " + id + " to " + s); switch (s) { case Initialized: run.setStatus(RemoteStatus.Initialized); break; case Operating: if (run.getStatus() == RemoteStatus.Initialized) { if (!factory.isAllowingRunsToStart()) throw new OverloadedException(); secContext.conveySecurity(); } run.setGenerateProvenance(generateProvenance); run.setStatus(RemoteStatus.Operating); factory.getMasterEventFeed().started(this, "started run execution", "The execution of run '" + getName() + "' has started."); break; case Stopped: run.setStatus(RemoteStatus.Stopped); break; case Finished: run.setStatus(RemoteStatus.Finished); break; } return null; } catch (IllegalStateTransitionException e) { throw new BadStateChangeException(e.getMessage()); } catch (RemoteException e) { throw new BadStateChangeException(e.getMessage(), e.getCause()); } catch (GeneralSecurityException | IOException e) { throw new BadStateChangeException(e.getMessage(), e); } catch (ImplementationException e) { if (e.getCause() != null) throw new BadStateChangeException(e.getMessage(), e.getCause()); throw new BadStateChangeException(e.getMessage(), e); } catch (StillWorkingOnItException e) { log.info("still working on setting status of run " + id + " to " + s, e); return e.getMessage(); } catch (InterruptedException e) { throw new BadStateChangeException("interrupted while waiting to insert notification into database"); } } static void checkBadFilename(String filename) throws FilesystemAccessException { if (filename.startsWith("/")) throw new FilesystemAccessException("filename may not be absolute"); if (Arrays.asList(filename.split("/")).contains("..")) throw new FilesystemAccessException("filename may not refer to parent"); } @Override public String getInputBaclavaFile() { try { return run.getInputBaclavaFile(); } catch (RemoteException e) { log.warn("problem when fetching input baclava file", e); return null; } } @Override public List<Input> getInputs() { ArrayList<Input> inputs = new ArrayList<>(); try { for (RemoteInput ri : run.getInputs()) inputs.add(new RunInput(ri)); } catch (RemoteException e) { log.warn("problem when fetching list of workflow inputs", e); } return inputs; } @Override public String getOutputBaclavaFile() { try { return run.getOutputBaclavaFile(); } catch (RemoteException e) { log.warn("problem when fetching output baclava file", e); return null; } } @Override public Input makeInput(String name) throws BadStateChangeException { try { return new RunInput(run.makeInput(name)); } catch (RemoteException e) { throw new BadStateChangeException("failed to make input", e); } } @Override public void setInputBaclavaFile(String filename) throws FilesystemAccessException, BadStateChangeException { checkBadFilename(filename); try { run.setInputBaclavaFile(filename); } catch (RemoteException e) { throw new FilesystemAccessException("cannot set input baclava file name", e); } } @Override public void setOutputBaclavaFile(String filename) throws FilesystemAccessException, BadStateChangeException { checkBadFilename(filename); try { run.setOutputBaclavaFile(filename); } catch (RemoteException e) { throw new FilesystemAccessException("cannot set output baclava file name", e); } } @Override public Date getCreationTimestamp() { return creationInstant == null ? null : new Date(creationInstant.getTime()); } @Override public Date getFinishTimestamp() { try { return run.getFinishTimestamp(); } catch (RemoteException e) { log.info("failed to get finish timestamp", e); return null; } } @Override public Date getStartTimestamp() { try { return run.getStartTimestamp(); } catch (RemoteException e) { log.info("failed to get finish timestamp", e); return null; } } /** * @param readers * the readers to set */ public void setReaders(Set<String> readers) { this.readers = new HashSet<>(readers); db.flushToDisk(this); } /** * @return the readers */ public Set<String> getReaders() { return readers == null ? new HashSet<String>() : unmodifiableSet(readers); } /** * @param writers * the writers to set */ public void setWriters(Set<String> writers) { this.writers = new HashSet<>(writers); db.flushToDisk(this); } /** * @return the writers */ public Set<String> getWriters() { return writers == null ? new HashSet<String>() : unmodifiableSet(writers); } /** * @param destroyers * the destroyers to set */ public void setDestroyers(Set<String> destroyers) { this.destroyers = new HashSet<>(destroyers); db.flushToDisk(this); } /** * @return the destroyers */ public Set<String> getDestroyers() { return destroyers == null ? new HashSet<String>() : unmodifiableSet(destroyers); } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeUTF(secContext.getOwner().getName()); out.writeObject(secContext.getFactory()); out.writeObject(new MarshalledObject<>(run)); } @Override public boolean getGenerateProvenance() { return generateProvenance; } @Override public void setGenerateProvenance(boolean generateProvenance) { this.generateProvenance = generateProvenance; db.flushToDisk(this); } @SuppressWarnings("unchecked") private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); if (log == null) log = getLog("Taverna.Server.LocalWorker"); final String creatorName = in.readUTF(); SecurityContextFactory factory = (SecurityContextFactory) in.readObject(); try { secContext = factory.create(this, new UsernamePrincipal(creatorName)); } catch (RuntimeException | IOException e) { throw e; } catch (Exception e) { throw new SecurityContextReconstructionException(e); } run = ((MarshalledObject<RemoteSingleRun>) in.readObject()).get(); } public void setSecurityContext(TavernaSecurityContext tavernaSecurityContext) { secContext = tavernaSecurityContext; } @Override public String getName() { return name; } @Override public void setName(@Nonnull String name) { if (name.length() > RunConnection.NAME_LENGTH) this.name = name.substring(0, RunConnection.NAME_LENGTH); else this.name = name; db.flushToDisk(this); } @Override public void ping() throws UnknownRunException { try { run.ping(); } catch (RemoteException e) { throw new UnknownRunException(e); } } } abstract class DEDelegate implements DirectoryEntry { Log log = getLog("Taverna.Server.Worker"); private RemoteDirectoryEntry entry; private String name; private String full; private Date cacheModTime; private long cacheQueryTime = 0L; DEDelegate(RemoteDirectoryEntry entry) { this.entry = entry; } @Override public void destroy() throws FilesystemAccessException { try { entry.destroy(); } catch (IOException e) { throw new FilesystemAccessException("failed to delete directory entry", e); } } @Override public String getFullName() { if (full != null) return full; String n = getName(); RemoteDirectoryEntry re = entry; try { while (true) { RemoteDirectory parent = re.getContainingDirectory(); if (parent == null) break; n = parent.getName() + "/" + n; re = parent; } } catch (RemoteException e) { log.warn("failed to generate full name", e); } return (full = n); } @Override public String getName() { if (name == null) try { name = entry.getName(); } catch (RemoteException e) { log.error("failed to get name", e); } return name; } @Override public Date getModificationDate() { if (cacheModTime == null || currentTimeMillis() - cacheQueryTime < 5000) try { cacheModTime = entry.getModificationDate(); cacheQueryTime = currentTimeMillis(); } catch (RemoteException e) { log.error("failed to get modification time", e); } return cacheModTime; } @Override public int compareTo(DirectoryEntry de) { return getFullName().compareTo(de.getFullName()); } @Override public boolean equals(Object o) { return o != null && o instanceof DEDelegate && getFullName().equals(((DEDelegate) o).getFullName()); } @Override public int hashCode() { return getFullName().hashCode(); } } class DirectoryDelegate extends DEDelegate implements Directory { RemoteDirectory rd; DirectoryDelegate(RemoteDirectory dir) { super(dir); rd = dir; } @Override public Collection<DirectoryEntry> getContents() throws FilesystemAccessException { ArrayList<DirectoryEntry> result = new ArrayList<>(); try { for (RemoteDirectoryEntry rde : rd.getContents()) { if (rde instanceof RemoteDirectory) result.add(new DirectoryDelegate((RemoteDirectory) rde)); else result.add(new FileDelegate((RemoteFile) rde)); } } catch (IOException e) { throw new FilesystemAccessException("failed to get directory contents", e); } return result; } @Override public Collection<DirectoryEntry> getContentsByDate() throws FilesystemAccessException { ArrayList<DirectoryEntry> result = new ArrayList<>(getContents()); sort(result, new DateComparator()); return result; } static class DateComparator implements Comparator<DirectoryEntry> { @Override public int compare(DirectoryEntry a, DirectoryEntry b) { return a.getModificationDate().compareTo(b.getModificationDate()); } } @Override public File makeEmptyFile(Principal actor, String name) throws FilesystemAccessException { try { return new FileDelegate(rd.makeEmptyFile(name)); } catch (IOException e) { throw new FilesystemAccessException("failed to make empty file", e); } } @Override public Directory makeSubdirectory(Principal actor, String name) throws FilesystemAccessException { try { return new DirectoryDelegate(rd.makeSubdirectory(name)); } catch (IOException e) { throw new FilesystemAccessException("failed to make subdirectory", e); } } @Override public ZipStream getContentsAsZip() throws FilesystemAccessException { ZipStream zs = new ZipStream(); final ZipOutputStream zos; try { zos = new ZipOutputStream(new PipedOutputStream(zs)); } catch (IOException e) { throw new FilesystemAccessException("problem building zip stream", e); } Thread t = new Thread(new Runnable() { @Override public void run() { try { zipDirectory(rd, null, zos); } catch (IOException e) { log.warn("problem when zipping directory", e); } finally { closeQuietly(zos); } } }); t.setDaemon(true); t.start(); return zs; } /** * Compresses a directory tree into a ZIP. * * @param dir * The directory to compress. * @param base * The base name of the directory (or <tt>null</tt> if this is * the root directory of the ZIP). * @param zos * Where to write the compressed data. * @throws RemoteException * If some kind of problem happens with the remote delegates. * @throws IOException * If we run into problems with reading or writing data. */ void zipDirectory(RemoteDirectory dir, String base, ZipOutputStream zos) throws RemoteException, IOException { for (RemoteDirectoryEntry rde : dir.getContents()) { String name = rde.getName(); if (base != null) name = base + "/" + name; if (rde instanceof RemoteDirectory) { RemoteDirectory rd = (RemoteDirectory) rde; zipDirectory(rd, name, zos); } else { RemoteFile rf = (RemoteFile) rde; zos.putNextEntry(new ZipEntry(name)); try { int off = 0; while (true) { byte[] c = rf.getContents(off, 64 * 1024); if (c == null || c.length == 0) break; zos.write(c); off += c.length; } } finally { zos.closeEntry(); } } } } } class FileDelegate extends DEDelegate implements File { RemoteFile rf; FileDelegate(RemoteFile f) { super(f); this.rf = f; } @Override public byte[] getContents(int offset, int length) throws FilesystemAccessException { try { return rf.getContents(offset, length); } catch (IOException e) { throw new FilesystemAccessException("failed to read file contents", e); } } @Override public long getSize() throws FilesystemAccessException { try { return rf.getSize(); } catch (IOException e) { throw new FilesystemAccessException("failed to get file length", e); } } @Override public void setContents(byte[] data) throws FilesystemAccessException { try { rf.setContents(data); } catch (IOException e) { throw new FilesystemAccessException("failed to write file contents", e); } } @Override public void appendContents(byte[] data) throws FilesystemAccessException { try { rf.appendContents(data); } catch (IOException e) { throw new FilesystemAccessException("failed to write file contents", e); } } @Override public void copy(File from) throws FilesystemAccessException { FileDelegate fromFile; try { fromFile = (FileDelegate) from; } catch (ClassCastException e) { throw new FilesystemAccessException("different types of File?!"); } try { rf.copy(fromFile.rf); } catch (Exception e) { throw new FilesystemAccessException("failed to copy file contents", e); } return; } } class ListenerDelegate implements Listener { private Log log = getLog("Taverna.Server.Worker"); private RemoteListener r; String conf; ListenerDelegate(RemoteListener l) { r = l; } RemoteListener getRemote() { return r; } @Override public String getConfiguration() { try { if (conf == null) conf = r.getConfiguration(); } catch (RemoteException e) { log.warn("failed to get configuration", e); } return conf; } @Override public String getName() { try { return r.getName(); } catch (RemoteException e) { log.warn("failed to get name", e); return "UNKNOWN NAME"; } } @Override public String getProperty(String propName) throws NoListenerException { try { return r.getProperty(propName); } catch (RemoteException e) { throw new NoListenerException("no such property: " + propName, e); } } @Override public String getType() { try { return r.getType(); } catch (RemoteException e) { log.warn("failed to get type", e); return "UNKNOWN TYPE"; } } @Override public String[] listProperties() { try { return r.listProperties(); } catch (RemoteException e) { log.warn("failed to list properties", e); return new String[0]; } } @Override public void setProperty(String propName, String value) throws NoListenerException, BadPropertyValueException { try { r.setProperty(propName, value); } catch (RemoteException e) { log.warn("failed to set property", e); if (e.getCause() != null && e.getCause() instanceof RuntimeException) throw new NoListenerException("failed to set property", e.getCause()); if (e.getCause() != null && e.getCause() instanceof Exception) throw new BadPropertyValueException("failed to set property", e.getCause()); throw new BadPropertyValueException("failed to set property", e); } } } class RunInput implements Input { private final RemoteInput i; RunInput(RemoteInput remote) { this.i = remote; } @Override public String getFile() { try { return i.getFile(); } catch (RemoteException e) { return null; } } @Override public String getName() { try { return i.getName(); } catch (RemoteException e) { return null; } } @Override public String getValue() { try { return i.getValue(); } catch (RemoteException e) { return null; } } @Override public void setFile(String file) throws FilesystemAccessException, BadStateChangeException { checkBadFilename(file); try { i.setFile(file); } catch (RemoteException e) { throw new FilesystemAccessException("cannot set file for input", e); } } @Override public void setValue(String value) throws BadStateChangeException { try { i.setValue(value); } catch (RemoteException e) { throw new BadStateChangeException(e); } } @Override public String getDelimiter() { try { return i.getDelimiter(); } catch (RemoteException e) { return null; } } @Override public void setDelimiter(String delimiter) throws BadStateChangeException { try { if (delimiter != null) delimiter = delimiter.substring(0, 1); i.setDelimiter(delimiter); } catch (RemoteException e) { throw new BadStateChangeException(e); } } } @SuppressWarnings("serial") class SecurityContextReconstructionException extends RuntimeException { public SecurityContextReconstructionException(Throwable t) { super("failed to rebuild security context", t); } }