/*
 * Filename: Ninja.java
 *
 * Author: Joe Wang
 *
 * Copyright (c) 2003-2005 
 * The Trustees of Columbia University in the City of New York
 * Columbia University Information Technology
 *
 * This file is part of NINJa.
 *
 * NINJa is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * NINJa is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NINJa; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */
package edu.columbia.print.ninja.core.service;

import java.util.zip.*;
import net.jini.lookup.entry.*;
import org.jini.rio.entry.WARDescriptor;
import org.jini.rio.web.EntryPoint;
import org.jini.rio.web.Gateway;
import edu.columbia.print.ninja.core.authenticate.*;
import net.jini.core.lease.Lease;
import net.jini.core.event.*;
import net.jini.core.entry.*;
import edu.columbia.print.ninja.core.service.Ninja.Emptier;
import javax.print.DocFlavor;
import javax.print.attribute.*;
import java.rmi.*;
import java.rmi.server.*;
import java.util.*;
import java.io.*;
import net.jini.print.service.*;
import java.util.Map;
import com.millipad.util.BlockingStack;
import com.sun.jini.lease.landlord.*;
import net.jini.print.job.*;
import edu.columbia.print.ninja.core.event.PrintQueueEvent;
import net.jini.lease.*;
import net.jini.core.lease.*;
import java.net.*;
import com.sun.rio.services.watchsmith.*;
import net.jini.core.lookup.*;
import javax.print.attribute.standard.*;
import java.util.prefs.*;
import com.sun.rio.resources.jsb.*;
import net.jini.print.lookup.*;

/**
 * Holds the pseudo queue.
 * @author Joe Wang <jw168@columbia.edu>
 * @since 1.0
 */
public class Ninja extends com.sun.rio.resources.jsb.ServiceBeanAdapter
implements Admin, RequestQueue, RemoteEventListener, net.jini.lease.DesiredExpirationListener, CancelListener {
    
    private static final long serialVersionUID = 1940526665672024429L;
    
    long currentId;
    String name;
    transient ResourceBundle bundle;
    transient java.util.Timer timer;
    transient File dir;
    transient Map printers;
    transient java.rmi.server.RemoteStub remote;
    transient LeasePolicy authPolicy, psPolicy, listPolicy, qPolicy;
    transient MultiLeasedResource authLand, psLand, listLand, qLand;
    transient net.jini.lease.LeaseRenewalManager listLeaseRenewer;
    transient BlockingStack authedJobs, stopStack, loadStack, authStack;
    transient com.millipad.util.RemoteBlockingStack startStack;
    transient Lookup lookup;
    transient RequestInserter server;
    transient java.nio.ByteBuffer xferBuffer;
    transient com.sun.rio.services.watchsmith.WatchsmithFinder watchFinder = null;
    transient UptimeWatch uptimeWatch = null;
    volatile long pjSeqNo, psSeqNo, pqSeqNo, prSeqNo;
    transient Landlord listLandlord, psLandlord, authLandlord, qLandlord;
    transient ServiceID id = null;
    transient Object proxy = null;
    transient Feed feed;
    transient Vector registrationBin = new Vector();
    transient Entry[] defaultEntries;
    transient RemoteNotifier notifier;
    
    public Ninja() throws Exception {
        super();
        bundle = ResourceBundle.getBundle(
            "edu.columbia.print.ninja.core.resource"
            );

        this.currentId = 0;
        this.pjSeqNo = 0;
        this.psSeqNo = 0;
        this.pqSeqNo = 0;
        this.prSeqNo = 0;
        
        setProperty("retainProperties", System.getProperty("ninja.retainProperties"));
        lookup = new Lookup();
        timer = new Timer(true);
        xferBuffer = java.nio.ByteBuffer.allocateDirect(4096);
        
        notifier = new RemoteNotifier(10);
        
        startStack = new com.millipad.util.RemoteBlockingStackImpl();
        UnicastRemoteObject.exportObject(startStack);
        stopStack = new BlockingStack();
        
        server = new RequestInserter(this);
        for(int i=0; i<10; i++) {
            Thread t = new Thread(server);
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
        }
        
        feed = new Feed(this, startStack);
        startStack = (com.millipad.util.RemoteBlockingStack )RemoteObject.toStub(startStack);
    }
    
    public static Object create(String logDirname) throws Exception {
        if(System.getSecurityManager()==null) {
            System.setSecurityManager(new java.rmi.RMISecurityManager());
        }
        Ninja list = new Ninja();
        File file = new File(logDirname);
        file = file.getParentFile();
        list.setLogDir(file);
        return null;
    }
    
    public void setLogDir(File dir) throws IOException {
        dir.mkdirs();
        this.dir = dir;
        start();
    }
    
    public void initialize(org.jini.rio.jsb.ServiceBeanContext context) throws Exception {
        super.initialize(context);
        InetAddress addr = InetAddress.getLocalHost();
        name = addr.getCanonicalHostName();
        
        Preferences pref = Preferences.userNodeForPackage(getClass()).parent();
        long msb = pref.getLong("service.id.msb", 0L);
        long lsb = pref.getLong("service.id.lsb", 0L);
        Calendar refreshTime = Calendar.getInstance();
        refreshTime.setTimeInMillis(pref.getLong("service.id.time", 0L));
        refreshTime.add(Calendar.DAY_OF_YEAR, 1);
        if(msb!=0L && lsb!=0L && refreshTime.after(Calendar.getInstance())) id = new ServiceID(msb, lsb);
        
        
        try {
            //register myself with the lookup service
            proxy = RemoteObject.toStub(this);
            Address address = new Address();
            address.organization = getProperty("address.organization");
            Gateway gateway;
            gateway = new Gateway();
            gateway.addEntryPoint(new EntryPoint("HTML", "selector.jsp", EntryPoint.JSP));
            String host = InetAddress.getLocalHost().getCanonicalHostName();
            WARDescriptor warConfig = new WARDescriptor(new URL(urlbase+"ninja.war"), gateway, "NINJa");
            net.jini.core.entry.Entry[] entries = {
                new net.jini.lookup.entry.ServiceInfo("NINJa","Columbia",
                "Columbia","1.0","",""),
                new AttributeEntry(new PrinterURI(new URI("lpd://"+getName()+"/public"))),
                address,
                warConfig
            };
            if(id==null) {
                id = register(proxy, entries);
                pref.putLong("service.id.msb", id.getMostSignificantBits());
                pref.putLong("service.id.lsb", id.getLeastSignificantBits());
                pref.putLong("service.id.time", Calendar.getInstance().getTimeInMillis());
                pref.flush();
            }
            else register(proxy, entries, id);
            defaultEntries = joiner.getEntrySet();
            joiner.getJoinManager().addAttributes(new Entry[] {new AttributeEntry(PrinterState.IDLE)});
            //                        joiner.getJoinManager().addAttributes();
            debug(id);

        feed.setRetries(Integer.parseInt(getProperty("job.failure.retries")));
        Thread t1 = new Thread(feed);
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        
            
            long renewDuration, defaultDuration, maxDuration;

            server.setCompress("true".equals(getProperty("request.compress")));
            renewDuration = Long.parseLong(getProperty("request.lease.renewDuration"));
            defaultDuration = Long.parseLong(getProperty("request.lease.defaultDuration"));
            maxDuration = Long.parseLong(getProperty("request.lease.defaultDuration"));
            listLand = new MultiLeasedResource("requestQueue");
            listLandlord = new GenericLandlord(listLand, this, renewDuration);		//local, do not export
            listPolicy = new LeaseDurationPolicy(maxDuration, defaultDuration, listLandlord, null, null);

            
            renewDuration = Long.parseLong(getProperty("printServiceListener.lease.renewDuration"));
            defaultDuration = Long.parseLong(getProperty("printServiceListener.lease.defaultDuration"));
            maxDuration = Long.parseLong(getProperty("printServiceListener.lease.defaultDuration"));
            psLand = new MultiLeasedResource("printServiceListener");
            psLandlord = new GenericLandlord(psLand, null, renewDuration);
            psPolicy = new LeaseDurationPolicy(maxDuration, defaultDuration, (Landlord )UnicastRemoteObject.exportObject(psLandlord), null, null);
        
            renewDuration = Long.parseLong(getProperty("queueListener.lease.renewDuration"));
            defaultDuration = Long.parseLong(getProperty("queueListener.lease.defaultDuration"));
            maxDuration = Long.parseLong(getProperty("queueListener.lease.defaultDuration"));
            qLand = new MultiLeasedResource("requestQueueListner");
            qLandlord = new GenericLandlord(qLand, null, renewDuration);
            qPolicy = new LeaseDurationPolicy(maxDuration, defaultDuration, (Landlord )UnicastRemoteObject.exportObject(qLandlord), null, null);
        
            renewDuration = Long.parseLong(getProperty("authenticator.lease.renewDuration"));
            defaultDuration = Long.parseLong(getProperty("authenticator.lease.defaultDuration"));
            maxDuration = Long.parseLong(getProperty("authenticator.lease.defaultDuration"));
            authLand = new MultiLeasedResource("authenticatorListener");
            authLandlord = new GenericLandlord(authLand, this, renewDuration);
            authPolicy = new LeaseDurationPolicy(maxDuration, defaultDuration, (Landlord )UnicastRemoteObject.exportObject(authLandlord), null, null);
        
        listLeaseRenewer = new LeaseRenewalManager();
        printers = new HashMap();
        
        //auth
        authedJobs = new BlockingStack();
        authStack = new BlockingStack();
        setQueueDir(getProperty("queue.dir"));
        }
        catch(Exception e) {
            debug(e + ": Unable to join LUS");
            e.printStackTrace();
        }
    }
    
    public void destroy() {
        
        super.destroy();
    }
    
    public EventRegistration addEventListener(Class[] categories, PrintQueueEvent[] events, final RemoteEventListener l, MarshalledObject handback, long duration) throws RemoteException {
        try {
            Lease lease = qPolicy.leaseFor(qLand.acquireResource(l), duration);
            EventRegistration reg = new EventRegistration(0L, null, lease, pqSeqNo);
            final MarshalledObject[] docAttrs = {null};
            //                    final Ninja parent = this;
            //                    final PrintServiceAttributeSet finalSet = set;
            timer.schedule(new TimerTask() {
                public void run() {
                    try {
                        for(Iterator i=listLand.values(); i.hasNext();) {
                            Object[] v = (Object[] )i.next();
                            RemoteDocPrintRequest req = (RemoteDocPrintRequest )v[1];
                            try {
                                loadJob(req, null, docAttrs, null, null);
                                addJob(l, req, (DocAttributeSet )docAttrs[0].get(), PrintQueueEvent.REQUEST_ADDED);
                            }
                            catch(Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }, 0);
            return reg;
        }
        catch(Exception e) {
            throw new RemoteException("addEventListener pq", e);
        }
    }
    
    //corresponds to attributes of ninja itself, not attached printers
    public PrintServiceAttributeSet getAttributes() throws RemoteException {
        PrintServiceAttributeSet set = lookup.getDefaultPrintService().getAttributes();
        if(set==null) set=new HashPrintServiceAttributeSet();
        set.add(new PrinterName(getName(), null));
        if(feed.isPrinting()) set.add(PrinterState.PROCESSING);
        return set;
    }
    
    protected void finalize() throws Throwable {
        UnicastRemoteObject.unexportObject(remote, true);
    }
    
    void addJob(RemoteDocPrintRequest req, DocAttributeSet doc, long requestState) {
        pqSeqNo++;
        for(Iterator i=qLand.values(); i.hasNext();) {
            RemoteEventListener l = (RemoteEventListener )i.next();
            if(l==null) {
                debug("addJob() listener is null");
                continue;
            }
            addJob(l, req, doc, requestState);
        }
    }
    
    void addJob(RemoteEventListener l, RemoteDocPrintRequest req, DocAttributeSet doc, long requestState) {
        PrintQueueEvent event = null;
        MarshalledObject mo;
        event = new PrintQueueEvent((PrintService )this, requestState, pqSeqNo, null, req, doc);
        notifier.notify(l, event);
        if(req!=null) {
            //PrintRequestAttribute
        }
    }
    
    public PrintQueueEvent[] getRequests() throws RemoteException {
        Vector events = new Vector();
        MarshalledObject[] docAttrs = {null};
        for(Iterator i=listLand.values(); i.hasNext();) {
            Object[] v = (Object[] )i.next();
            final PrintRequest req = (PrintRequest )v[1];
            try {
                loadJob(req, null, docAttrs, null, null);
                events.add(new PrintQueueEvent(this, PrintQueueEvent.REQUEST_ADDED, pqSeqNo, null, req, (DocAttributeSet )docAttrs[0].get()));
            }
            catch(Exception e) {
                debug("getRequests:"+e);
            }
        }
        if(events.size()==0) return null;
        return (PrintQueueEvent[] )events.toArray(new PrintQueueEvent[events.size()]);
    }
    
    void removeRequest(RemoteDocPrintRequest job) {
        pqSeqNo++;
        try {debug("remove \""+job.getAttributes().get(JobName.class)+"\"");}
        catch(Exception e) {e.printStackTrace();}
        for(Iterator i=qLand.values(); i.hasNext();) {
            RemoteEventListener l = (RemoteEventListener )i.next();
            removeRequest(l, job);
        }
    }
    
    void removeRequest(RemoteEventListener l, RemoteDocPrintRequest job) {
        PrintQueueEvent event = new PrintQueueEvent(this, PrintQueueEvent.REQUEST_REMOVED, pqSeqNo++, null, job, null);
        notifier.notify(l, event);
    }
    
    /**
     * load persisted jobs
     */
    void start() {
        String[] files = dir.list();
        if(!dir.exists()) {
            debug("The jobs directory ("+dir.getPath()
            +") must be created first");
            System.exit(-1);
        }
        long time = System.currentTimeMillis();
        java.util.Set unique = new java.util.HashSet();
        //		PrintServiceProxy proxy = getProxy();
        for(int i=files.length-1; i>=0; i--) {
            String name = null;
            try {
                Lease[] lease = {null};
                MarshalledObject[] docAttrs = {null};
                name = files[i].substring(0, files[i].length()-2);
                if(unique.contains(name)) continue;
                unique.add(name);
                ListRequest req = new ListRequest(name, null, server);
                loadJob(req, lease, docAttrs, null, null);
                long duration = lease[0].getExpiration() - time;
                duration = Math.max(duration, 0);
                
                if(duration<=0) {
                    new File(dir, name+"cf").delete();
                    new File(dir, name+"df").delete();
                    continue;
                }
                
                RemoteDocPrintRequest stub = (RemoteDocPrintRequest )UnicastRemoteObject.exportObject(req);
                lease[0] =  listPolicy.leaseFor(
                listLand.acquireResource(new Object[] {req,stub}),
                duration
                );
                insertRequest(req, stub, (DocAttributeSet )docAttrs[0].get(), lease[0]);
            }
            catch(ClassCastException e) {
                if(name==null) continue;
                debug("invalid job "+name);
                try {
                    new File(dir, name+"cf").delete();
                    new File(dir, name+"df").delete();
                }
                catch(Exception ex2) {
                    debug(ex2);
                }
            }
            catch(Exception e) {
                debug(files[i]+" "+e);
                try {
                    new File(dir, name+"cf").delete();
                    new File(dir, name+"df").delete();
                }
                catch(Exception ex2) {
                    debug("remove bad file "+ex2);
                }
            }
        }
    }
    
    public String getName() throws RemoteException {return name;}
    
    /**
     * add a job to the current state
     * @param job the job to add
     * @param set the request attributes
     * @param doc the doc being printed for the job
     * @param lease the lease associated with the job
     */
    void insertRequest(RemoteDocPrintRequest req, RemoteDocPrintRequest stub, DocAttributeSet doc, Lease lease) {

        //insert
        try {
            if(uptimeWatch==null && watchFinder!=null && watchFinder.isWatchAvailable()) {
                com.sun.rio.resources.watcher.StopWatch watch = watchFinder.getWatch(getName()+": Uptime");
                if(watch!=null) {
                    uptimeWatch = new UptimeWatch(watch.getWatchDataSource(), getName()+": Uptime");
                    uptimeWatch.setPeriod(60*60*1000);		//every hour
                    uptimeWatch.start();
                }
            }
        }
        catch(Exception ex) {
            debug("insert request "+ex);
            uptimeWatch = null;
        }
        addJob(stub, doc, PrintQueueEvent.REQUEST_ADDED);
        listLeaseRenewer.renewFor(lease, Lease.FOREVER, Lease.ANY, this);
    }

    void insertingRequest(RemoteDocPrintRequest stub, DocAttributeSet doc, Lease lease) {
        addJob(stub, doc, PrintQueueEvent.REQUEST_ADDING);
    }

    public void notify(net.jini.lease.LeaseRenewalEvent event) {
//        debug("can't renew doc "+event.getLease());
//        removeRequest(RemoteDocPrintRequest job);
        Vector events = new Vector();
        MarshalledObject[] docAttrs = {null};
        for(Iterator i=listLand.leasedResources(); i.hasNext();) {
            LeasedResource rsrc = (LeasedResource )i.next();
            if(listPolicy.ensureCurrent(rsrc)) continue;
            Object[] v = (Object[] )listLand.getValue(rsrc.getCookie());
            removeRequest((RemoteDocPrintRequest )v[1]);
        }
    }
    
    /**
     * persist a job to the filesystem
     * @param job the print job
     * @param lease the lease associated with the job
     * @param reqAttrs the request attributes
     * @param flavor the doc flavor information
     * @param docAttrs the doc attributes
     * @param doc a byte channel with the document data
     */
    void storeJob(PrintRequest req, Lease lease, DocAttributeSet docAttrs, DocFlavor flavor, java.nio.channels.ReadableByteChannel doc) throws IOException {
        boolean hasCF = false;
        boolean hasDF = false;
        String name = req.toString();
        if(flavor!=null || docAttrs!=null || doc!=null) hasDF = true;
        if(lease!=null) lease.setSerialFormat(Lease.ABSOLUTE);
        if(hasDF) {
            FileOutputStream os = new FileOutputStream(new File(dir, name+"df"));
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(flavor);
            oos.writeObject(new MarshalledObject(docAttrs));
            java.nio.channels.FileChannel channel = os.getChannel();
            for(long position=channel.position(); doc.isOpen();) {
                long delta = channel.transferFrom(doc, position,
                Integer.MAX_VALUE);
                position += delta;
                if(delta>0) continue;
                for(;;) {
                    xferBuffer.clear();
                    int count = doc.read(xferBuffer);
                    if(count<0) break;
                    debug(count+" count");
                    xferBuffer.flip();
                    channel.write(xferBuffer);
                }
                break;
            }
            os.close();
        } {
            FileOutputStream os = new FileOutputStream(new File(dir, name+"cf"));
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(req);
            oos.writeObject(lease);
            oos.close();
        }
    }
    
    /**
     * load a job from the filesystem
     * @param job the job to load
     * @param lease the lease container
     * @param reqAttrs the attribute holder
     * @param docAttrs the attribute holder
     * @param flavor the flavor holder
     * @param doc the inputstream holder
     */
    void loadJob(PrintRequest request, Lease[] lease, MarshalledObject[] docAttrs, DocFlavor[] flavor, FileInputStream[] doc) throws IOException, ClassNotFoundException {
        boolean hasCF = false;
        boolean hasDF = false;
        String name = request.toString();
        if(!(request instanceof ListRequest)) {
            for(Iterator i=listLand.values(); i.hasNext();) {
                Object[] v = (Object[] )i.next();
                if(!v[1].equals(request)) continue;
                name = v[0]+"";
                break;
            }
        }
        //		if(lease!=null || reqAttrs!=null) hasCF = true;
        if(flavor!=null || docAttrs!=null || doc!=null) hasDF = true;
        //		if(hasCF)
        {
            FileInputStream is = new FileInputStream(new File(dir, name+"cf"));
            ObjectInputStream ois = new ObjectInputStream(is);
            Object tmp;
            tmp = ois.readObject();
            ListRequest r = (ListRequest )tmp;
            request.setAttributes(r.getAttributes());
            tmp = ois.readObject();
            if(lease!=null) lease[0] = (Lease )tmp;
            ois.close();
        }
        if(hasDF) {
            FileInputStream is = new FileInputStream(new File(dir, name+"df"));
            ObjectInputStream ois = new ObjectInputStream(is);
            Object tmp;
            tmp = ois.readObject();
            if(flavor!=null) flavor[0] = (DocFlavor )tmp;
            tmp = ois.readObject();
            if(docAttrs!=null) docAttrs[0] = (MarshalledObject )tmp;
            if(doc!=null) doc[0] = is;
        }
    }
    
    public synchronized String getId() {
        //make sure does not already exist
        currentId++;
        for(;; currentId++) {
            File df = new File(dir, currentId+"df");
            if(df.exists()) continue;
            File cf = new File(dir, currentId+"cf");
            if(cf.exists()) continue;
            break;
        }
        return currentId+"";
    }
    
    public AuthStub requestStart(PrintRequest[] jobs, PrintService ps, long duration) throws RemoteException, net.jini.core.lease.LeaseDeniedException {
        //remove dead leases
        long time = -System.currentTimeMillis();
        int counter = 0;
        for(Iterator i=authLand.leasedResources(); i.hasNext();) {
            LeasedResource rsrc = (LeasedResource )i.next();
            counter++;
            if(authPolicy.ensureCurrent(rsrc)) continue;
            Object auth = authLand.getValue(rsrc.getCookie());
            authStack.push(auth);
            authLand.releaseResource(rsrc.getCookie());
            try {
                authLandlord.cancel(rsrc.getCookie());
            }
            catch(Exception e) {
                e.printStackTrace();
            }
        }
        MarshalledObject[] dtmpAttrs = new MarshalledObject[1];
        MarshalledObject[] docAttrs = new MarshalledObject[jobs.length];
        int count = 0;
        
        for(int i=jobs.length-1; i>=0; i--) {
            try {
                dtmpAttrs[0] = null;
                PrintRequestAttributeSet set = null;
                set=((edu.columbia.print.ninja.core.service.RemoteDocPrintRequest )jobs[i]).getAttributes();
                loadJob(jobs[i], null, dtmpAttrs, null, null);
                if(set!=null) jobs[i].setAttributes(set);
                docAttrs[i] = dtmpAttrs[0];
                count++;
            }
            catch(Exception e) {
                debug(e);
                jobs[i] = null;
                docAttrs[i] = null;
                e.printStackTrace();
            }
        }
        if(count==0) throw new LeaseDeniedException("no valid jobs");
        
        MarshalledObject auth;
        auth = (MarshalledObject )authStack.pop();
        if(auth==null) debug(authStack.size()+" size");
        Lease lease=authPolicy.leaseFor(authLand.acquireResource(auth), duration);
        if(ps==null) {
            ps = lookup.getDefaultPrintService();
            if(ps instanceof RemotePrintService) {
                RemotePrintService stub = (RemotePrintService )RemoteObject.toStub((RemotePrintService )ps);
                printers.put(stub, ps);
                ps = stub;
            }
        }
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        AuthStub stub = new AuthStub(auth, startStack, jobs, docAttrs, lease, ps, true);
        return stub;
    }
    
    /**
     * Invoked by the Authenticator to indicate which jobs have been authenticated
     */
    public void jobsAuthenticated(javax.print.DocPrintJob[] jobs, Object cookie, edu.columbia.print.ninja.core.authenticate.Authenticator auth) throws RemoteException {
        for(int i=jobs.length-1; i>=0; i--) {
            authedJobs.push(new Object[] {jobs[i], cookie, auth});
        }
    }
    
    private static final void debug(Object i) {com.millipad.util.Debug.print(Ninja.class, i);}
    
    public AuthStub requestRemove(final PrintRequest[] jobs, long duration) throws RemoteException {
        //should return the auth stub first
        //the auth stub should control removal
        timer.schedule(new TimerTask() {
            public void run() {
                for(int i=jobs.length-1; i>=0; i--) {
                    for(Iterator j=listLand.leasedResources(); j.hasNext();) {
                        LeasedResource rsrc = (LeasedResource )j.next();
                        Object[] v = (Object[] )listLand.getValue(rsrc.getCookie());
                        if(!jobs[i].equals(v[1])) continue;
                        try {
                            listLandlord.cancel(rsrc.getCookie());
                            break;
                        }
                        catch(Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, 0);
        return null;
    }
    
    public void setAuthenticators(edu.columbia.print.ninja.core.authenticate.Authenticator[] auths) throws RemoteException {
        try {
            for(MarshalledObject mo=null;(mo=(MarshalledObject )authStack.pop(100))!=null;);// debug("remove: "+mo.get());
            for(int i = auths.length-1; i>=0; i--) {
//                debug("add: "+auths[i]);
                authStack.push(new MarshalledObject(auths[i]));
            }
        }
        catch(IOException e) {
            throw new RemoteException("unable to setAuthenticators", e);
        }
    }
    
    public void reboot() throws RemoteException {
        try {
            Process p = Runtime.getRuntime().exec("/sbin/reboot -f");
            new Thread(new Emptier(p.getErrorStream())).start();
            new Thread(new Emptier(p.getInputStream())).start();
        }
        catch(Exception e) {
        }
    }
    
    public void restart() throws RemoteException {
        System.exit(1);
    }
    
    public void setQueueDir(final String file) throws RemoteException {
        new Thread() {
            public void run() {
                try {
                    System.out.println("setQueueDir:"+file);
                    setLogDir(new File(file));
                }
                catch(IOException e) {
                    debug("set q dir"+e);
                    return;
                }
            }
        }.start();
        return;
    }
    
    public String getQueueDir() throws RemoteException {
        return getProperty("queue.dir");
    }
    
    public String control(String cmd) throws RemoteException {
        if(cmd==null) return null;
        //		PrintService ps = lookup.getDefaultPrintService();
        cmd = cmd.trim();
        if(cmd.length()==0) return null;
        try {
            Process p = Runtime.getRuntime().exec("/usr/sbin/lpc "+cmd);
            new Thread(new Emptier(p.getErrorStream())).start();
            BufferedReader reader = new BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
            StringBuffer buffer = new StringBuffer();
            for(;;) {
                String tmp = reader.readLine();
                if(tmp==null) break;
                buffer.append(tmp+"\n");
            }
            return buffer.toString();
        }
        catch(Exception e) {
            debug("stat "+e);
        }
        return null;
    }
    
    public void setProperty(String prop, String value) throws RemoteException {
        Preferences pref = Preferences.userNodeForPackage(getClass()).parent();
        try {
            if("retainPreferences".equals(prop)) {
                if("true".equals(value)) pref.putBoolean(prop, true);
                else pref.clear();
                pref.flush();
                return;
            }
            if(!"true".equals(getProperty("retainPreferences"))) return;
            if("request.lease.defaultDuration".equals(prop)) {
                long renewDuration = pref.getLong("request.lease.renewDuration", -1);	//default to not renewable
                long maxDuration = pref.getLong("request.lease.defaultDuration", 24*60*60*1000);
                pref.putLong("request.lease.defaultDuration", Long.parseLong(value));
                listPolicy = new LeaseDurationPolicy(maxDuration, Long.parseLong(value), listLandlord, null, null);
                pref.flush();
                return;
            }
            if("job.failure.retries".equals(prop)) {
                pref.putInt("job.failure.retries", Integer.parseInt(value));
                feed.setRetries(Integer.parseInt(value));
                pref.flush();
                return;
            }
            if("request.compress".equals(prop)) {
                pref.putBoolean("request.compress", "true".equals(value));
                server.setCompress("true".equals(value));
                pref.flush();
                return;
            }
            if("queue.dir".equals(prop)) {
                pref.put(prop, value);
                pref.flush();
                return;
            }
            if("address.organization".equals(prop)) {
                pref.put(prop, value);
                Address update = new Address();
                update.organization = value;
                joiner.getJoinManager().modifyAttributes(
                new Entry[] {new Address()},
                new Entry[] {update}
                );
                pref.flush();
                return;
            }
            if("request.lease.renewDuration".equals(prop)
            || "request.lease.defaultDuration".equals(prop)
            || "printServiceListener.lease.renewDuration".equals(prop)
            || "printServiceListener.lease.defaultDuration".equals(prop)
            || "queueListener.lease.renewDuration".equals(prop)
            || "queueListener.lease.defaultDuration".equals(prop)
            || "authenticator.lease.renewDuration".equals(prop)
            || "authenticator.lease.defaultDuration".equals(prop)
            ) {
                pref.putLong(prop, Long.parseLong(value));
                pref.flush();
                return;
            }
        }
        catch(BackingStoreException e) {
            throw new RemoteException("can't store request.failure.retries", e);
        }
    }
    
    public String[] getPropertyKeys() throws RemoteException {
        return new String[] {
            "retainPreferences","request.compress","request.lease.renewDuration","request.lease.defaultDuration",
            "printServiceListener.lease.renewDuration","printServiceListener.lease.defaultDuration",
            "queueListener.lease.renewDuration","queueListener.lease.defaultDuration",
            "authenticator.lease.renewDuration","authenticator.lease.defaultDuration",
            "job.failure.retries","address.organization","queue.dir"
        };
    }
    
    public String getProperty(String prop) throws RemoteException {
        Preferences pref = Preferences.userNodeForPackage(getClass()).parent();
        try {
            if("request.compress".equals(prop)
            || "retainPreferences".equals(prop)
            ) {
                return pref.getBoolean(prop, "true".equals(System.getProperty("ninja."+prop)))+"";
            }
            if("request.lease.renewDuration".equals(prop)
            || "request.lease.defaultDuration".equals(prop)
            || "printServiceListener.lease.renewDuration".equals(prop)
            || "printServiceListener.lease.defaultDuration".equals(prop)
            || "queueListener.lease.renewDuration".equals(prop)
            || "queueListener.lease.defaultDuration".equals(prop)
            || "authenticator.lease.renewDuration".equals(prop)
            || "authenticator.lease.defaultDuration".equals(prop)
            ) {
                return pref.getLong(prop, Long.parseLong(System.getProperty("ninja."+prop)))+"";
            }
            if("job.failure.retries".equals(prop)) {
                return pref.getInt(prop, Integer.parseInt(System.getProperty("ninja."+prop)))+"";
            }
            if("address.organization".equals(prop)
            || "queue.dir".equals(prop)
            ) {
                return pref.get(prop, System.getProperty("ninja."+prop));
            }
        }
        catch(NumberFormatException e) {
        }
        debug("missing property: "+prop);
        return null;
    }
    
    public boolean registerServiceProvider(Lookup sp) throws RemoteException {
        boolean result = lookup.registerServiceProvider(sp);
        PrintService ps = lookup.getDefaultPrintService();
        try {
            ps.addEventListener(null, null, (RemoteEventListener )RemoteObject.toStub(this), null, 0L);
        }
        catch(Exception e) {
            e.printStackTrace();
            throw new RemoteException("could not register event listener with default print service", e);
        }
        return result;
    }
    
    public EventRegistration addEventListener(Class[] theCategories, javax.print.attribute.PrintServiceAttribute[] theValues, final RemoteEventListener theListener, MarshalledObject theHandbackObject, long theRequestedLeaseDuration) throws RemoteException {
        EventRegistration reg = null;
        PrintService service = lookup.getDefaultPrintService();
        PrintServiceAttributeSet sset = service.getAttributes();
        PrintServiceAttributeSet set = new HashPrintServiceAttributeSet();
        if(sset!=null) set.addAll(sset);
        set.add(new PrinterName(getName(), null));
        try {
            debug("ps listeners "+theListener);
            Lease lease = psPolicy.leaseFor(psLand.acquireResource(theListener), theRequestedLeaseDuration);
            reg = new EventRegistration(0L, null, lease, 0L);
        }
        catch(Exception e) {
            e.printStackTrace();
            return null;
        }
        try {
            notifier.notify(theListener, new PrintServiceEvent(this, 0L, 0L, null, set));
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        return reg;
    }
    
    public DocPrintRequest createDocPrintRequest() throws PrintServiceException, RemoteException {
        try {
            ListRequest req = null;
            String id = getId();
            req = new ListRequest(
            id,
            (PrintService )RemoteObject.toStub(this),
            server
            );
            return req;
        }
        catch(Exception e) {
            debug("create doc print request "+e);
        }
        return null;
    }
    
    public DocPrintRequest createDocPrintRequest(net.jini.print.data.Doc theDoc, javax.print.attribute.PrintRequestAttributeSet theAttributes) throws PrintServiceException, RemoteException {
        return null;
    }
    
    public Attribute getDefaultAttributeValue(Class theCategory, Settings theSettings) throws RemoteException {
        return null;
    }
    
    public javax.print.attribute.AttributeSet getDefaultAttributeValues(Settings theSettings) throws RemoteException {
        return null;
    }
    
    public Class[] getSupportedAttributeCategories(Settings theSettings) throws RemoteException {
        return null;
    }
    
    public Object getSupportedAttributeValues(Class theCategory, Settings theSettings) throws RemoteException {
        return null;
    }
    
    public DocFlavor[] getSupportedDocFlavors(Settings theSettings) throws RemoteException {
        return null;
    }
    
    public Settings getUnsupportedSettings(Settings theSettings) throws RemoteException {
        return null;
    }
    
    public boolean isAttributeCategorySupported(Class theCategory, Settings theSettings) throws RemoteException {
        return false;
    }
    
    public boolean isAttributeValueSupported(Attribute theValue, Settings theSettings) throws RemoteException {
        return false;
    }
    
    public boolean isDocFlavorSupported(DocFlavor theFlavor, Settings theSettings) throws RemoteException {
        return false;
    }
    
    public void notify(net.jini.core.event.RemoteEvent remoteEvent) throws UnknownEventException, RemoteException {
        if(remoteEvent instanceof PrintServiceEvent) {
            PrintServiceEvent event = (PrintServiceEvent )remoteEvent;
            Attribute[] attrs = null;
            try {
                PrintServiceAttributeSet set = new HashPrintServiceAttributeSet();
                set.addAll(event.getAttributes());
                attrs = event.getAttributes().toArray();
                set.add(new PrinterName(getName(), null));
                event = new PrintServiceEvent(this, 0L, 0L, null, set);
            }
            catch(Exception e) {
                attrs = new Attribute[0];
//                e.printStackTrace();
            }
            try {
                for(Iterator i = psLand.values(); i.hasNext();) {
                    RemoteEventListener l = (RemoteEventListener )i.next();
                    notifier.notify(l, event);
                }
            }
            catch(Exception e) {
                e.printStackTrace();
            }
            //update the attributes
            try {
                final Entry[] updates = new Entry[attrs.length];
                for(int i=attrs.length-1; i>=0; i--) {
                    //debug(attrs[i]);
                    updates[i]=new AttributeEntry(attrs[i]);
                }
                registrationBin.add(new Object[] {proxy, updates});
                timer.schedule(new TimerTask() {
                    public void run() {
                        Object[] v = null;
                        try {
                            v = (Object[])registrationBin.remove(0);
                        }
                        catch(ArrayIndexOutOfBoundsException boundsException) {
                        }
                        if(v==null) return;
                        try {
                            Set set = new HashSet();
                            for(int i=defaultEntries.length-1; i>=0; i--) set.add(defaultEntries[i]);
                            for(int i=((Entry[] )v[1]).length-1; i>=0; i--) set.add(((Entry[] )v[1])[i]);
                            joiner.getJoinManager().setAttributes((Entry[] )set.toArray(new Entry[set.size()]));
                        }
                        catch(Exception e) {
                            e.printStackTrace();
                        }
                    }
                }, 0);
            }
            catch(Exception e) {
            }
            return;
        }
    }
    
    public void expirationReached(net.jini.lease.LeaseRenewalEvent leaseRenewalEvent) {
        debug("expire");
    }
    
    public void leaseCancelled(Object value, MultiLeasedResource rsrc) {
        if(rsrc==listLand) {
            Object[] v = (Object[] )value;
            RemoteDocPrintRequest req = (RemoteDocPrintRequest )v[1];
            removeRequest(req);
//            try {((ListRequest_Stub )req).clearListeners();}
//            catch(Exception e) {debug("can't cleanup "+e);}
            try {new File(dir, v[0]+"cf").delete();}
            catch(Exception e) {debug(e);}
            try {new File(dir, v[0]+"df").delete();}
            catch(Exception e) {debug(e);}
            try {UnicastRemoteObject.unexportObject((Remote )v[0], true);}
            catch(Exception e) {e.printStackTrace();}
            return;
        }
        if(rsrc==authLand) {
            if(value==null) return;
            authStack.push(value);
            //System.out.println("auth removed");
            return;
        }
        debug("leaseCancelled? from "+rsrc);
    }
    
    public long getTimeout(int type) throws RemoteException {
        return 0L;
    }
    
    public void setTimeout(int type, long value) throws RemoteException {
    }
    
    public long getRequestExpiration(PrintRequest request) throws RemoteException {
        for(Iterator i=listLand.leasedResources(); i.hasNext();) {
            LeasedResource rsrc = (LeasedResource )i.next();
            Object[] v = (Object[] )listLand.getValue(rsrc.getCookie());
            if(!v[1].equals(request)) continue;
            return rsrc.getExpiration();
        }
        throw new RemoteException("don't know the expiration");
    }
    
    class Emptier implements Runnable {
        InputStream is;
        Emptier(InputStream is) { this.is = is; }
        public void run() {
            try {
                for(int b = 0; b>=0;) b=is.read();
                is.close();
            }
            catch(IOException e) {
            }
        }
    }
    
}
