org.taverna.server.master.worker.RemoteRunDelegate.java Source code

Java tutorial

Introduction

Here is the source code for org.taverna.server.master.worker.RemoteRunDelegate.java

Source

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