com.zimbra.cs.zimlet.ZimletUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.zimlet.ZimletUtil.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
 *
 * This program 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,
 * version 2 of the License.
 *
 * This program 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 this program.
 * If not, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */
package com.zimbra.cs.zimlet;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.PartBase;

import com.zimbra.common.account.Key;
import com.zimbra.common.account.ProvisioningConstants;
import com.zimbra.common.auth.ZAuthToken;
import com.zimbra.common.httpclient.HttpClientUtil;
import com.zimbra.common.localconfig.DebugConfig;
import com.zimbra.common.localconfig.LC;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.soap.AccountConstants;
import com.zimbra.common.soap.AdminConstants;
import com.zimbra.common.soap.Element;
import com.zimbra.common.soap.Element.XMLElement;
import com.zimbra.common.soap.MailConstants;
import com.zimbra.common.soap.SoapHttpTransport;
import com.zimbra.common.soap.SoapTransport;
import com.zimbra.common.soap.W3cDomUtil;
import com.zimbra.common.util.ByteUtil;
import com.zimbra.common.util.CliUtil;
import com.zimbra.common.util.FileUtil;
import com.zimbra.common.util.Pair;
import com.zimbra.common.util.StringUtil;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.AccountServiceException;
import com.zimbra.cs.account.Cos;
import com.zimbra.cs.account.Domain;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.account.Server;
import com.zimbra.cs.account.Zimlet;
import com.zimbra.cs.account.soap.SoapProvisioning;
import com.zimbra.cs.httpclient.URLUtil;
import com.zimbra.cs.service.admin.FlushCache;
import com.zimbra.cs.util.BuildInfo;
import com.zimbra.cs.util.WebClientServiceUtil;
import com.zimbra.cs.zimlet.ZimletPresence.Presence;
import com.zimbra.soap.admin.type.CacheEntryType;

/**
 *
 * @author jylee
 *
 */
public class ZimletUtil {

    public static final String ZIMLET_BASE = "/service/zimlet";
    public static final String ZIMLET_DEV_DIR = "_dev";
    public static final String ZIMLET_ALLOWED_DOMAINS = "allowedDomains";
    public static final String ZIMLET_DEFAULT_COS = "default";
    public static final String PARAM_ZIMLET = "Zimlet";
    public static final String ZIMLET_NAME_REGEX = "^[\\w.-]+$";
    private static final String ZIMLET_CACHE_DIR = "/opt/zimbra/jetty/work/resource-cache/zimletres/latest";

    private static int P_MAX = Integer.MAX_VALUE;
    private static boolean sZimletsLoaded = false;
    private static Map<String, ZimletFile> sZimlets = new HashMap<String, ZimletFile>();

    private static Map<String, Class> sZimletHandlers = new HashMap<String, Class>();

    public static void migrateUserPrefIfNecessary(Account acct) throws ServiceException {
        if (!DebugConfig.enableMigrateUserZimletPrefs) {
            return;
        }

        Set<String> wanted = acct.getMultiAttrSet(Provisioning.A_zimbraPrefZimlets);
        Set<String> unwanted = acct.getMultiAttrSet(Provisioning.A_zimbraPrefDisabledZimlets);

        // needs upgrade only if wanted is not empty and unwanted is empty, because
        // if wanted is empty: means use whatever zimbraZimletAvailableZimlets/zimbraZimletDomainAvailableZimlets says
        // if unwanted is not empty: already migrated or user has change it.
        boolean needsMigrate = (!wanted.isEmpty()) && unwanted.isEmpty();

        if (!needsMigrate) {
            return;
        }

        ZimletPresence availZimlets = getAvailableZimlets(acct);
        Map<String, Object> attrs = new HashMap<String, Object>();
        StringBuilder disabledZimletNamesForLogging = new StringBuilder();

        for (String zimletName : availZimlets.getZimletNames()) {
            Presence presence = availZimlets.getPresence(zimletName);

            if (presence == Presence.enabled && !wanted.contains(zimletName)) {
                disabledZimletNamesForLogging.append(zimletName + ", ");
                StringUtil.addToMultiMap(attrs, Provisioning.A_zimbraPrefDisabledZimlets, zimletName);
            }
        }

        // in the odd case when someone has all available zimlets set in his zimbraPrefZimlets,
        // attrs will be empty => we've got nothing to do.
        if (attrs.isEmpty()) {
            ZimbraLog.account.info("Not migrating zimbraPrefDisabledZimlets for account " + acct.getName()
                    + " because all zimlets are enabled");
            return;
        }

        ZimbraLog.account.info("Migrating zimbraPrefDisabledZimlets for account " + acct.getName() + " to "
                + disabledZimletNamesForLogging.toString());

        Provisioning prov = Provisioning.getInstance();
        prov.modifyAttrs(acct, attrs);
    }

    public static ZimletPresence getUserZimlets(Account acct) throws ServiceException {
        ZimletPresence userZimlets = getAvailableZimlets(acct);

        // userZimlets now contains all allowed zimlets for the user
        // process user pref, which overrides cos/domain enabled/disabled by default
        // user pref cannot override cos/domain mandatory zimlets

        String[] userPrefEnabledZimlets = acct.getMultiAttr(Provisioning.A_zimbraPrefZimlets);
        for (String zimletName : userPrefEnabledZimlets) {
            Presence presence = userZimlets.getPresence(zimletName);
            if (presence != null && // zimlet is allowed
                    presence != Presence.mandatory && // can't override mandatory
                    presence != Presence.enabled) { // disabled by default, but user specifically enabled it
                userZimlets.put(zimletName, Presence.enabled);
            }
        }

        String[] userPrefDisabledZimlets = acct.getMultiAttr(Provisioning.A_zimbraPrefDisabledZimlets);
        for (String zimletName : userPrefDisabledZimlets) {
            Presence presence = userZimlets.getPresence(zimletName);
            if (presence != null && // zimlet is allowed
                    presence != Presence.mandatory && // can't override mandatory
                    presence != Presence.disabled) { // enabled by default, but user specifically disabled it
                userZimlets.put(zimletName, Presence.disabled);
            }
        }

        return userZimlets;
    }

    public static ZimletPresence getAvailableZimlets(Account acct) throws ServiceException {
        ZimletPresence availZimlets = new ZimletPresence();

        // process domain settings first, because if domain and cos conflict we honor the cos setting
        Domain domain = Provisioning.getInstance().getDomain(acct);
        if (domain != null) {
            String[] domainZimlets = domain.getMultiAttr(Provisioning.A_zimbraZimletDomainAvailableZimlets);
            for (String zimletWithPrefix : domainZimlets) {
                availZimlets.put(zimletWithPrefix);
            }
        }

        String[] acctCosZimlets = acct.getMultiAttr(Provisioning.A_zimbraZimletAvailableZimlets);
        for (String zimletWithPrefix : acctCosZimlets) {
            availZimlets.put(zimletWithPrefix);
        }

        return availZimlets;
    }

    public static ZimletPresence getAvailableZimlets(Cos cos) throws ServiceException {
        ZimletPresence availZimlets = new ZimletPresence();

        String[] acctCosZimlets = cos.getMultiAttr(Provisioning.A_zimbraZimletAvailableZimlets);
        for (String zimletWithPrefix : acctCosZimlets) {
            availZimlets.put(zimletWithPrefix);
        }

        return availZimlets;
    }

    public static String[] listZimletNames() {
        String[] zimlets = sZimlets.keySet().toArray(new String[0]);
        Arrays.sort(zimlets);
        return zimlets;
    }

    public static String[] listDevZimletNames() {
        String[] zimlets = loadDevZimlets().keySet().toArray(new String[0]);
        Arrays.sort(zimlets); // TODO: Should sort by zimlet priority.
        return zimlets;
    }

    public static List<Zimlet> orderZimletsByPriority(List<Zimlet> zimlets) {
        // create a sortable collection, sort, then return List<Zimlet> in the
        // sorted order.  version is not comparable in String format.
        List<Pair<Version, Zimlet>> plist = new ArrayList<Pair<Version, Zimlet>>();
        for (Zimlet z : zimlets) {
            String pstring = z.getPriority();
            if (pstring == null) {
                // no priority.  put it at the end of priority list
                pstring = Integer.toString(Integer.MAX_VALUE);
            }
            Version v = new Version(pstring);
            plist.add(new Pair<Version, Zimlet>(v, z));
        }
        Collections.sort(plist, new Comparator<Pair<Version, Zimlet>>() {
            @Override
            public int compare(Pair<Version, Zimlet> first, Pair<Version, Zimlet> second) {
                return first.getFirst().compareTo(second.getFirst());
            }
        });

        List<Zimlet> ret = new ArrayList<Zimlet>();
        for (Pair<Version, Zimlet> p : plist) {
            ret.add(p.getSecond());
        }
        return ret;
    }

    public static List<Zimlet> orderZimletsByPriority(String[] zimlets) {
        Provisioning prov = Provisioning.getInstance();
        List<Zimlet> zlist = new ArrayList<Zimlet>();
        for (int i = 0; i < zimlets.length; i++) {
            try {
                Zimlet z = prov.getZimlet(zimlets[i]);
                if (z != null) {
                    zlist.add(z);
                }
            } catch (ServiceException se) {
                // ignore error and continue on
                ZimbraLog.zimlet.warn("unable to get zimlet " + zimlets[i], se);
            }
        }
        return orderZimletsByPriority(zlist);
    }

    public static List<Zimlet> orderZimletsByPriority() throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        List<Zimlet> allzimlets = prov.listAllZimlets();
        return orderZimletsByPriority(allzimlets);
    }

    public static void updateZimletConfig(String zimlet, String config) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        Zimlet zim = prov.getZimlet(zimlet);
        if (zim == null) {
            throw AccountServiceException.NO_SUCH_ZIMLET(zimlet);
        }
        Map<String, String> map = new HashMap<String, String>();
        map.put(Provisioning.A_zimbraZimletHandlerConfig, config);
        prov.modifyAttrs(zim, map);
    }

    public static ZimletConfig getZimletConfig(String zimlet) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        Zimlet z = prov.getZimlet(zimlet);
        if (z == null) {
            throw AccountServiceException.NO_SUCH_ZIMLET(zimlet);
        }
        String cf = z.getAttr(Provisioning.A_zimbraZimletHandlerConfig);
        if (cf == null) {
            return null;
        }
        try {
            return new ZimletConfig(cf);
        } catch (ZimletException e) {
            ZimbraLog.zimlet.warn("Unable to load zimlet config for " + z.getName(), e);
        }
        return null;
    }

    /**
     * Loads all the Zimlets, locates the server side ZimletHandler for each Zimlets,
     * loads the class and instantiate the object, then returns the instance.
     *
     * @param name of the Zimlet
     * @return ZimletHandler object
     */
    public static ZimletHandler getHandler(String name) {
        loadZimlets();
        Class zh = sZimletHandlers.get(name);
        if (zh == null) {
            ZimletFile zf = sZimlets.get(name);
            if (zf == null) {
                return null;
            }
            URLClassLoader cl = null;
            try {
                String clazz = zf.getZimletDescription().getServerExtensionClass();
                if (clazz != null) {
                    URL[] urls = { zf.toURL() };
                    cl = new URLClassLoader(urls, ZimletUtil.class.getClassLoader());
                    zh = cl.loadClass(clazz);
                    ZimbraLog.zimlet.info("Loaded class " + zh.getName());
                    sZimletHandlers.put(name, zh);
                }
            } catch (Exception e) {
                ZimbraLog.zimlet.warn("Unable to load zimlet handler for %s", name, e);
                return null;
            } finally {
                if (cl != null) {
                    try {
                        cl.close();
                    } catch (IOException e) {
                        ZimbraLog.zimlet.warn("failed to close URLClassLoader", e);
                    }
                }
            }
        }
        try {
            if (zh != null) {
                return (ZimletHandler) zh.newInstance();
            }
        } catch (Exception e) {
            ZimbraLog.zimlet.warn("Unable to instantiate zimlet handler for " + name, e);
        }
        return null;
    }

    public static ZimletFile getZimlet(String zimlet) {
        ZimletFile zf = null;
        loadZimlets();
        zf = sZimlets.get(zimlet);
        if (zf != null) {
            return zf;
        }
        return loadDevZimlets().get(zimlet);
    }

    /**
     *
     * Load all the installed Zimlets.
     *
     */
    public synchronized static Map<String, ZimletFile> loadZimlets() {
        if (!sZimletsLoaded) {
            loadZimletsFromDir(sZimlets, LC.zimlet_directory.value());
            sZimletsLoaded = true;
        }
        // NOTE: Was added for consistency with loadDevZimlets
        return sZimlets;
    }

    /**
     *
     * Load all the Zimlets in the dev test directory.
     *
     */
    public static Map<String, ZimletFile> loadDevZimlets() {
        Map<String, ZimletFile> zimletMap = new HashMap<String, ZimletFile>();
        loadZimletsFromDir(zimletMap, LC.zimlet_directory.value() + File.separator + ZIMLET_DEV_DIR);
        return zimletMap;
    }

    /**
     *
     * Throw away the cached Zimlet, and reloads from the file system.
     *
     * @param zimlet
     * @throws ZimletException
     */
    public static void reloadZimlet(String zimlet) throws ZimletException {
        ZimletFile zf;
        try {
            zf = new ZimletFile(LC.zimlet_directory.value() + File.separator + zimlet);
        } catch (IOException ioe) {
            ZimbraLog.zimlet.warn(ioe.getMessage());
            return;
        }
        synchronized (sZimlets) {
            sZimlets.remove(zimlet);
            sZimlets.put(zimlet, zf);
        }
    }

    /**
     *
     * Load all the Zimlets found in the directory.
     *
     * @param zimlets - Zimlet cache
     * @param dir - directory
     */
    private static void loadZimletsFromDir(Map<String, ZimletFile> zimlets, String dir) {
        File zimletRootDir = new File(dir);
        if (zimletRootDir == null || !zimletRootDir.exists() || !zimletRootDir.isDirectory()) {
            return;
        }

        ZimbraLog.zimlet.debug("Loading zimlets from " + zimletRootDir.getPath());

        synchronized (zimlets) {
            zimlets.clear();
            String[] zimletNames = zimletRootDir.list();
            assert (zimletNames != null);
            for (int i = 0; i < zimletNames.length; i++) {
                if (zimletNames[i].equals(ZIMLET_DEV_DIR)) {
                    continue;
                }
                try {
                    zimlets.put(zimletNames[i], new ZimletFile(zimletRootDir, zimletNames[i]));
                } catch (Exception e) {
                    ZimbraLog.zimlet.warn("error loading zimlet " + zimletNames[i], e);
                }
            }
        }
    }

    /**
     *
     * List the Zimlet description as XML or JSON Element.
     *
     * @param elem - Parent Element node
     * @param zimlet
     */
    public static void listZimlet(Element elem, Zimlet zimlet, int priority, Presence presence) {
        loadZimlets();
        ZimletFile zf = sZimlets.get(zimlet.getName());
        if (zf == null) {
            ZimbraLog.zimlet.warn("cannot find zimlet " + zimlet.getName());
            return;
        }
        String zimletBase = ZIMLET_BASE + "/" + zimlet.getName() + "/";
        Element entry = elem.addElement(AccountConstants.E_ZIMLET);
        Element zimletContext = entry.addElement(AccountConstants.E_ZIMLET_CONTEXT);
        zimletContext.addAttribute(AccountConstants.A_ZIMLET_BASE_URL, zimletBase);
        if (priority >= 0) {
            zimletContext.addAttribute(AccountConstants.A_ZIMLET_PRIORITY, priority);
        }

        if (presence == null) {
            presence = Presence.enabled;
        }
        zimletContext.addAttribute(AccountConstants.A_ZIMLET_PRESENCE, presence.toString());

        try {
            zf.getZimletDescription().addToElement(entry);
            String config = zimlet.getHandlerConfig();
            if (config != null) {
                entry.addElement(W3cDomUtil.parseXML(config, elem.getFactory()));
            }
        } catch (Exception e) {
            ZimbraLog.zimlet.warn("error loading zimlet " + zimlet, e);
        }
    }

    /**
     *
     * List the description of all the dev test Zimlets as Element.
     *
     * @param elem - Parent element node
     */
    public static void listDevZimlets(Element elem) {
        for (ZimletFile zim : loadDevZimlets().values()) {
            String zimletBase = ZIMLET_BASE + "/" + ZIMLET_DEV_DIR + "/" + zim.getZimletName() + "/";
            Element entry = elem.addElement(AccountConstants.E_ZIMLET);
            Element zimletContext = entry.addElement(AccountConstants.E_ZIMLET_CONTEXT);
            zimletContext.addAttribute(AccountConstants.A_ZIMLET_BASE_URL, zimletBase);

            // dev zimlets are all enabled
            zimletContext.addAttribute(AccountConstants.A_ZIMLET_PRESENCE, Presence.enabled.toString());

            try {
                zim.getZimletDescription().addToElement(entry);
                if (zim.hasZimletConfig()) {
                    zim.getZimletConfig().addToElement(entry);
                }
            } catch (Exception e) {
                ZimbraLog.zimlet.warn("error loading dev zimlet: " + zim.getName(), e);
            }
        }
    }

    private static Map<String, Object> descToMap(ZimletDescription zd) throws ZimletException {
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put(Provisioning.A_zimbraZimletKeyword, zd.getServerExtensionKeyword());
        attrs.put(Provisioning.A_zimbraZimletVersion, zd.getVersion().toString());
        attrs.put(Provisioning.A_zimbraZimletDescription, zd.getDescription());
        attrs.put(Provisioning.A_zimbraZimletHandlerClass, zd.getServerExtensionClass());
        attrs.put(Provisioning.A_zimbraZimletServerIndexRegex, zd.getRegexString());
        return attrs;
    }

    public static File getZimletRootDir(String zimletName) throws ZimletException {
        if (zimletName.matches(ZIMLET_NAME_REGEX)) {
            return new File(LC.zimlet_directory.value(), zimletName);
        } else {
            throw ZimletException.INVALID_ZIMLET_NAME();
        }
    }

    public static void flushCache() throws ZimletException {
        sZimletsLoaded = false;
        try {
            Provisioning.getInstance().flushCache(CacheEntryType.zimlet, null);
        } catch (ServiceException e) {
            throw ZimletException.CANNOT_FLUSH_CACHE(e);
        }
    }

    public static void flushDiskCache(Map<String, Object> context) throws ServiceException {
        if (WebClientServiceUtil.isServerInSplitMode()) {
            List<Server> allServers = Provisioning.getInstance().getAllServers();
            for (Server server : allServers) {
                if (server.hasMailClientService()) {
                    FlushCache.flushAllZimlets(context);
                } else {
                    WebClientServiceUtil.sendFlushZimletRequestToUiNode(server);
                }
            }
        }
    }

    public static void flushAllZimletsCache() {
        try {
            File file = new File(ZIMLET_CACHE_DIR);
            FileUtil.deleteDirContents(file);
        } catch (IOException e) {
            ZimbraLog.zimlet.warn("failed to flush zimlet cache", e);
        }
    }

    public static interface DeployListener {
        public void markFinished(Server s);

        public void markFailed(Server s, Exception e);
    }

    public static void deployZimletLocally(ZimletFile zf, DeployListener listener)
            throws IOException, ZimletException, ServiceException {
        Server localServer = Provisioning.getInstance().getLocalServer();
        deployZimletLocally(zf);
        if (listener != null) {
            listener.markFinished(localServer);
        }
    }

    enum Action {
        INSTALL, UPGRADE, REPAIR
    };

    /**
     * Deploys the specified Zimlet on local server, be it service node or ui node. The following actions are taken.
     * 1.  Install the Zimlet files on the machine.
     * 2.  Check the LDAP for the Zimlet entry.  If the entry already exists, stop.
     * 3.  Install the LDAP entry for the Zimlet.
     * 4.  Install Zimlet config.
     * 5.  Activate the zimlet on default COS.
     * 6.  Enable the Zimlet.
     *
     * @param zimlet
     * @throws IOException
     * @throws ZimletException
     */
    public static void deployZimletLocally(ZimletFile zf) throws IOException, ZimletException, ServiceException {
        Provisioning prov = Provisioning.getInstance();
        String zimletName = zf.getZimletName();
        ZimletDescription zd = zf.getZimletDescription();
        Zimlet z;
        Action action = Action.INSTALL;
        String priority = null;
        boolean enable = true;

        // check if the zimlet already exists in LDAP.
        z = prov.getZimlet(zimletName);

        if (z != null) {
            Version ver = new Version(z.getAttr(Provisioning.A_zimbraZimletVersion));
            if (zd.getVersion().compareTo(ver) < 0) {
                ZimbraLog.zimlet.info("Zimlet " + zimletName + " being installed is of an older version.");
            }
            if (zd.getVersion().compareTo(ver) == 0) {
                action = Action.REPAIR;
            } else {
                action = Action.UPGRADE;
            }
            // save priority
            priority = z.getPriority();
            enable = z.isEnabled();
        }

        // update LDAP
        z = ldapDeploy(zf);

        // install files
        installZimletLocally(zf);

        if (action == Action.REPAIR) {
            return;
        }

        // upgrade
        ZimbraLog.zimlet.info("Upgrading Zimlet " + zimletName + " to " + zd.getVersion().toString());

        // set the priority
        if (priority == null) {
            setPriority(zimletName, P_MAX);
        }

        // install the config
        if (zf.hasZimletConfig()) {
            installConfig(zf.getZimletConfig());
        }

        // activate
        if (!zd.isExtension()) {
            activateZimlet(zimletName, ZIMLET_DEFAULT_COS);
        }

        if (!enable) {
            // it was an upgrade of previously disabled zimlet.  leave it alone.
            return;
        }

        // enable
        enableZimlet(zimletName);
    }

    /**
     *
     * Install the Zimlet file on this machine.
     *
     * @param zimlet
     * @return
     * @throws IOException
     * @throws ZimletException
     */
    public static void installZimletLocally(ZimletFile zf) throws IOException, ZimletException {
        ZimletDescription zd = zf.getZimletDescription();
        String zimletName = zd.getName();
        ZimbraLog.zimlet.info("Installing Zimlet " + zimletName + " on this host.");

        // location for the jar files
        File libDir = new File(LC.mailboxd_directory.value() + File.separator + "webapps" + File.separator
                + "zimlet" + File.separator + "WEB-INF" + File.separator + "lib");
        // location for the rest of the files
        File zimlet = getZimletRootDir(zimletName);

        zimlet.getParentFile().mkdirs();
        if (zimlet.exists()) {
            deleteFile(zimlet);
        }

        for (ZimletFile.ZimletEntry entry : zf.getAllEntries()) {
            String fname = entry.getName();
            File file;
            if (fname.endsWith(".jar")) {
                file = new File(libDir, fname);
                if (!file.getCanonicalPath().startsWith(libDir.getCanonicalPath())) {
                    ZimbraLog.zimlet
                            .error(String.format("Zimlet %s has an invalid file path %s", zimletName, fname));
                    throw ZimletException.CANNOT_DEPLOY(zimletName, "Invalid file path " + fname, null);
                }
            } else {
                file = new File(zimlet, fname);
                if (!file.getCanonicalPath().startsWith(LC.zimlet_directory.value())) {
                    ZimbraLog.zimlet
                            .error(String.format("Zimlet %s has an invalid file path %s", zimletName, fname));
                    throw ZimletException.CANNOT_DEPLOY(zimletName, "Invalid file path " + fname, null);
                }
            }

            file.getParentFile().mkdirs();
            writeFile(entry.getContents(), file);
        }

        flushCache();
    }

    /**
     *
     * Deploy the Zimlet to LDAP.
     *
     * @param zimlet
     * @throws IOException
     * @throws ZimletException
     */
    public static void ldapDeploy(String zimlet) throws ServiceException, IOException, ZimletException {
        String zimletRoot = LC.zimlet_directory.value();
        ZimletFile zf = new ZimletFile(zimletRoot + File.separator + zimlet);
        ldapDeploy(zf);
    }

    public static Zimlet ldapDeploy(ZimletFile zf) throws ServiceException, IOException, ZimletException {
        ZimletDescription zd = zf.getZimletDescription();
        String zimletName = zd.getName();
        Map<String, Object> attrs = descToMap(zd);
        List<String> targets = zd.getTargets();
        if (targets != null && targets.size() > 0) {
            attrs.put(Provisioning.A_zimbraZimletTarget, targets);
        }

        String disableZimletUndeploy = zd.getDisableUIUndeploy();
        if (disableZimletUndeploy != null && disableZimletUndeploy.equalsIgnoreCase("true")) {
            attrs.put(Provisioning.A_zimbraAdminExtDisableUIUndeploy, ProvisioningConstants.TRUE);
        }

        if (zd.isExtension()) {
            attrs.put(Provisioning.A_zimbraZimletIsExtension, ProvisioningConstants.TRUE);
        } else {
            attrs.put(Provisioning.A_zimbraZimletIsExtension, ProvisioningConstants.FALSE);
        }

        ZimbraLog.zimlet.info("Deploying Zimlet " + zimletName + " in LDAP.");

        // add zimlet entry to ldap
        Provisioning prov = Provisioning.getInstance();
        Zimlet zim = prov.getZimlet(zimletName);
        if (zim == null) {
            zim = prov.createZimlet(zimletName, attrs);
        } else {
            prov.modifyAttrs(zim, attrs);
        }
        return zim;
    }

    private static void writeFile(byte[] src, File dest) throws IOException {
        dest.createNewFile();
        ByteArrayInputStream bais = new ByteArrayInputStream(src);
        FileOutputStream fos = new FileOutputStream(dest);
        ByteUtil.copy(bais, true, fos, true);
    }

    private static void deleteFile(File f) {
        if (f.isDirectory()) {
            for (File sub : f.listFiles()) {
                deleteFile(sub);
            }
        }
        f.delete();
    }

    /**
     *
     * Delete the Zimlet from LDAP and remove the associated Zimlet files.
     *
     * @param zimlet
     * @throws ZimletException
     */
    public static void undeployZimletLocally(String zimlet) throws ServiceException {
        ZimbraLog.zimlet.info("Uninstalling Zimlet " + zimlet + " from LDAP.");
        Provisioning prov = Provisioning.getInstance();
        Zimlet z = prov.getZimlet(zimlet);
        if (z != null) {
            List<Cos> cos = prov.getAllCos();
            for (Cos c : cos) {
                try {
                    deactivateZimlet(zimlet, c.getName());
                } catch (Exception e) {
                    ZimbraLog.zimlet.warn("Error deactiving Zimlet " + zimlet + " in LDAP.", e);
                }
            }
            try {
                prov.deleteZimlet(zimlet);
            } catch (ServiceException se) {
                z = prov.getZimlet(zimlet);
                if (z != null) {
                    ZimbraLog.zimlet.warn("Error deleting Zimlet " + zimlet + " in LDAP.", se);
                }
            }
        }

        ZimletFile zf = sZimlets.get(zimlet);
        if (zf != null) {
            sZimlets.remove(zimlet);
        }
        ZimbraLog.zimlet.info("undeploying zimlet %s", zimlet);
        try {
            File zimletDir = ZimletUtil.getZimletRootDir(zimlet);
            FileUtil.deleteDir(zimletDir);
            ZimbraLog.zimlet.info("zimlet directory %s is deleted", zimletDir.getName());
        } catch (IOException e) {
            throw ServiceException.FAILURE("error occurred when deleting zimlet directory", e);
        } catch (ZimletException e) {
            throw ServiceException.FAILURE("error occurred when deleting zimlet directory", e);
        }
    }

    /**
     *
     * Add the Zimlet to specified COS.
     *
     * @param zimlet
     * @param cos
     * @throws ZimletException
     */
    public static void activateZimlet(String zimlet, String cos) throws ServiceException, ZimletException {
        ZimbraLog.zimlet.info("Adding Zimlet " + zimlet + " to COS " + cos);
        Provisioning prov = Provisioning.getInstance();
        Cos c = prov.get(Key.CosBy.name, cos);
        if (c == null) {
            throw ZimletException.CANNOT_ACTIVATE("no such cos " + cos, null);
        }
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("+" + Provisioning.A_zimbraZimletAvailableZimlets, zimlet);
        prov.modifyAttrs(c, attrs);
        ZimletConfig zc = getZimletConfig(zimlet);
        if (zc == null) {
            return;
        }
        String allowedDomains = zc.getConfigValue(ZIMLET_ALLOWED_DOMAINS);
        if (allowedDomains != null) {
            addAllowedDomains(allowedDomains, cos);
        }
    }

    /**
     *
     * Remove the Zimlet from COS.
     *
     * @param zimlet
     * @param cos
     * @throws ZimletException
     */
    public static void deactivateZimlet(String zimlet, String cos) throws ServiceException, ZimletException {
        ZimbraLog.zimlet.info("Removing Zimlet " + zimlet + " from COS " + cos);
        Provisioning prov = Provisioning.getInstance();
        Cos c = prov.get(Key.CosBy.name, cos);
        if (c == null) {
            throw ZimletException.CANNOT_DEACTIVATE("no such cos " + cos, null);
        }
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("-" + Provisioning.A_zimbraZimletAvailableZimlets, zimlet);
        prov.modifyAttrs(c, attrs);
        ZimletConfig zc = getZimletConfig(zimlet);
        if (zc == null) {
            return;
        }
        String domains = zc.getConfigValue(ZIMLET_ALLOWED_DOMAINS);
        if (domains == null) {
            return;
        }
        String[] domainArray = domains.toLowerCase().split(",");
        Set<String> domainsToRemove = new HashSet<String>();
        for (String d : domainArray) {
            domainsToRemove.add(d);
        }
        String[] zimlets = getAvailableZimlets(c).getZimletNamesAsArray();
        for (String z : zimlets) {
            if (z.equals(zimlet)) {
                continue;
            }
            zc = getZimletConfig(z);
            if (zc == null) {
                continue;
            }
            domains = zc.getConfigValue(ZIMLET_ALLOWED_DOMAINS);
            if (domains == null) {
                continue;
            }
            domainArray = domains.toLowerCase().split(",");
            for (String d : domainArray) {
                domainsToRemove.remove(d);
            }
        }
        if (!domainsToRemove.isEmpty()) {
            removeAllowedDomains(domainsToRemove, cos);
        }
    }

    /**
     *
     * Change the enabled status of the Zimlet.
     *
     * @param zimlet
     * @param enabled
     * @throws ZimletException
     */
    public static void setZimletEnable(String zimlet, boolean enabled) throws ZimletException {
        Provisioning prov = Provisioning.getInstance();
        try {
            Zimlet z = prov.getZimlet(zimlet);
            if (z == null) {
                throw AccountServiceException.NO_SUCH_ZIMLET(zimlet);
            }
            Map<String, String> attr = new HashMap<String, String>();
            attr.put(Provisioning.A_zimbraZimletEnabled,
                    enabled ? ProvisioningConstants.TRUE : ProvisioningConstants.FALSE);
            prov.modifyAttrs(z, attr);
        } catch (Exception e) {
            if (enabled) {
                throw ZimletException.CANNOT_ENABLE(zimlet, e);
            } else {
                throw ZimletException.CANNOT_DISABLE(zimlet, e);
            }
        }
    }

    /**
     *
     * Enable the Zimlet.  Only the enabled Zimlets are available to the users.
     *
     * @param zimlet
     * @throws ZimletException
     */
    public static void enableZimlet(String zimlet) throws ZimletException {
        ZimbraLog.zimlet.info("Enabling Zimlet " + zimlet);
        setZimletEnable(zimlet, true);
    }

    /**
     *
     * Disable the Zimlet.  Disabled Zimlets are not available to the users.
     *
     * @param zimlet
     * @throws ZimletException
     */
    public static void disableZimlet(String zimlet) throws ZimletException {
        ZimbraLog.zimlet.info("Disabling Zimlet " + zimlet);
        setZimletEnable(zimlet, false);
    }

    /**
     *
     * Change the Zimlet COS ACL.
     *
     * @param zimlet
     * @param args
     * @throws ZimletException
     */
    public static void aclZimlet(String zimlet, String[] args) throws ServiceException, ZimletException {
        for (int i = 2; i < args.length; i += 2) {
            String cos = args[i];
            String action = args[i + 1].toLowerCase();
            if (action.equals("grant")) {
                activateZimlet(zimlet, cos);
            } else if (action.equals("deny")) {
                deactivateZimlet(zimlet, cos);
            } else {
                throw ZimletException.ZIMLET_HANDLER_ERROR("invalid acl command " + args[i + 1]);
            }
        }
    }

    /**
     *
     * List all the COS the Zimlet is available to.
     *
     * @param zimlet
     * @throws ZimletException
     */
    public static void listAcls(String zimlet) throws ServiceException, ZimletException {
        System.out.println("Listing COS entries for Zimlet " + zimlet + "...");
        Provisioning prov = Provisioning.getInstance();
        for (Cos cos : prov.getAllCos()) {
            String[] zimlets = getAvailableZimlets(cos).getZimletNamesAsArray();
            for (int i = 0; i < zimlets.length; i++) {
                if (zimlets[i].equals(zimlet)) {
                    System.out.println("\t" + cos.getName());
                    break;
                }
            }
        }
    }

    /**
     *
     * Print all the Zimlets installed on this host.
     *
     * @throws ZimletException
     */
    public static void listInstalledZimletsOnHost(boolean everything) {
        loadZimlets();
        ZimletFile[] zimlets = sZimlets.values().toArray(new ZimletFile[0]);
        Arrays.sort(zimlets);
        for (int i = 0; i < zimlets.length; i++) {
            ZimletDescription zd;
            System.out.print("\t" + zimlets[i].getZimletName());
            try {
                zd = zimlets[i].getZimletDescription();
                boolean isExtension = (zd != null && zd.isExtension());
                if (isExtension && everything) {
                    System.out.print(" (ext)");
                }
            } catch (Exception e) {
                ZimbraLog.zimlet.warn("error reading zimlet : " + zimlets[i].getName(), e);
            }
            System.out.println();
        }
    }

    /**
     *
     * Print all the Zimlets on LDAP.
     *
     * @throws ZimletException
     */
    public static void listInstalledZimletsInLdap(boolean everything) throws ServiceException, ZimletException {
        Provisioning prov = Provisioning.getInstance();
        for (Zimlet z : prov.listAllZimlets()) {
            boolean isExtension = z.isExtension();
            if (!everything && isExtension) {
                continue;
            }
            System.out.print("\t" + z.getName());
            if (!z.isEnabled()) {
                System.out.print(" (disabled)");
            }
            if (isExtension) {
                System.out.print(" (ext)");
            }
            System.out.println();
        }
    }

    /**
     *
     * Print the Zimlet COS ACL for all the Zimlets.
     *
     * @throws ZimletException
     */
    public static void listZimletsInCos() throws ServiceException, ZimletException {
        Provisioning prov = Provisioning.getInstance();
        for (Cos cos : prov.getAllCos()) {
            System.out.println("  " + cos.getName() + ":");
            String[] zimlets = getAvailableZimlets(cos).getZimletNamesAsArray();
            Arrays.sort(zimlets);
            for (int i = 0; i < zimlets.length; i++) {
                System.out.println("\t" + zimlets[i]);
            }
        }
    }

    /**
     *
     * Print all the Zimlets installed on the host, on LDAP, and COS ACL.
     *
     * @throws ZimletException
     */
    public static void listAllZimlets(boolean everything) throws ServiceException, ZimletException {
        System.out.println("Installed Zimlet files on this host:");
        listInstalledZimletsOnHost(everything);
        System.out.println("Installed Zimlets in LDAP:");
        listInstalledZimletsInLdap(everything);
        System.out.println("Available Zimlets in COS:");
        listZimletsInCos();
    }

    /**
     *
     * Dump the config template for the Zimlet.
     *
     * @param zimlet
     * @throws IOException
     * @throws ZimletException
     */
    public static void dumpConfig(String zimlet) throws IOException, ZimletException {
        ZimletFile zf = new ZimletFile(zimlet);
        String config = zf.getZimletConfigString();
        System.out.println(config);
    }

    /**
     *
     * Install the Zimlet configuration.
     *
     * @param config
     * @throws IOException
     * @throws ZimletException
     */
    private static void installConfig(ZimletConfig zc) throws ServiceException, IOException, ZimletException {
        String zimletName = zc.getName();
        ZimbraLog.zimlet.info("Installing Zimlet config for " + zimletName);
        String configString = zc.toXMLString();
        updateZimletConfig(zimletName, configString);
        String allowedDomains = zc.getConfigValue(ZIMLET_ALLOWED_DOMAINS);
        if (allowedDomains != null) {
            addAllowedDomains(allowedDomains, "default"); // XXX to default cos for now
        }
    }

    public static void installConfig(String config) throws ServiceException, IOException, ZimletException {
        installConfig(new ZimletConfig(config));
    }

    public static void addAllowedDomains(String domains, String cosName) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        Cos cos = prov.get(Key.CosBy.name, cosName);
        Set<String> domainSet = cos.getMultiAttrSet(Provisioning.A_zimbraProxyAllowedDomains);
        String[] domainArray = domains.toLowerCase().split(",");
        for (int i = 0; i < domainArray.length; i++) {
            domainSet.add(domainArray[i]);
        }
        Map<String, String[]> newlist = new HashMap<String, String[]>();
        newlist.put(Provisioning.A_zimbraProxyAllowedDomains, domainSet.toArray(new String[0]));
        prov.modifyAttrs(cos, newlist);
    }

    public static void removeAllowedDomains(Set<String> domains, String cosName) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        Cos cos = prov.get(Key.CosBy.name, cosName);
        Set<String> domainSet = cos.getMultiAttrSet(Provisioning.A_zimbraProxyAllowedDomains);
        String[] domainArray = domains.toArray(new String[0]);
        for (int i = 0; i < domainArray.length; i++) {
            domainSet.remove(domainArray[i]);
        }
        Map<String, String[]> newlist = new HashMap<String, String[]>();
        newlist.put(Provisioning.A_zimbraProxyAllowedDomains, domainSet.toArray(new String[0]));
        prov.modifyAttrs(cos, newlist);
    }

    public static void listPriority() throws ServiceException {
        List<Zimlet> plist = orderZimletsByPriority();
        System.out.println("Pri\tZimlet");
        for (int i = 0; i < plist.size(); i++) {
            System.out.println(i + "\t" + plist.get(i).getName());
        }
    }

    public static void setPriority(String zimlet, int priority) throws ServiceException {
        List<Zimlet> plist = orderZimletsByPriority();
        Provisioning prov = Provisioning.getInstance();
        Zimlet z = prov.getZimlet(zimlet);
        if (z == null) {
            throw AccountServiceException.NO_SUCH_ZIMLET(zimlet);
        }
        setPriority(z, priority, plist);
    }

    private static void setPriority(Zimlet zimlet, String priority) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        Map<String, String> attr = new HashMap<String, String>();
        attr.put(Provisioning.A_zimbraZimletPriority, priority);
        prov.modifyAttrs(zimlet, attr);
    }

    public static void setPriority(Zimlet z, int priority, List<Zimlet> plist) throws ServiceException {
        // remove self first
        // XXX LdapEntry.equals() is not implemented
        for (Zimlet zim : plist) {
            if (zim.compareTo(z) == 0) {
                plist.remove(zim);
                break;
            }
        }

        if (priority == P_MAX) {
            priority = plist.size();
        }
        Version newPriority;
        if (priority == 0) {
            newPriority = new Version("0");
            setPriority(z, newPriority.toString());
            plist.add(0, z);
            if (plist.size() > 1) {
                // make sure the previous p0 zimlet is now p1.
                Zimlet p0zimlet = plist.get(1);
                setPriority(p0zimlet, 1, plist);
            }
        } else {
            // take the priority of previous zimlet
            Zimlet oneAbove = plist.get(priority - 1);
            String pString = oneAbove.getPriority();
            if (pString == null) {
                // priority is mandatory now, but it could be from old version
                // when we didn't have priorities.
                pString = Integer.toString(priority);
            }
            newPriority = new Version(pString);
            if (priority < plist.size()) {
                // increment, while staying before the next zimlet
                Zimlet oneBelow = plist.get(priority);
                pString = oneBelow.getPriority();
                if (pString == null) {
                    pString = Integer.toString(priority + 2);
                }
                Version nextPriority = new Version(pString);
                if (newPriority.compareTo(nextPriority) < 0) {
                    newPriority.increment(nextPriority);
                } else {
                    // it really is an error because priorities of two zimlets
                    // shouldn't be the same.  bump the next one down
                    newPriority.increment();
                    setPriority(z, newPriority.toString());
                    plist.add(priority, z);
                    setPriority(oneBelow, priority + 1, plist);
                    return;
                }
            } else {
                // simply increment from the previous priority
                newPriority.increment();
            }
            setPriority(z, newPriority.toString());
        }

        try {
            flushCache();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ServiceException) {
                throw (ServiceException) t;
            }
            if (e instanceof IOException) {
                t = e;
            }
            throw ServiceException.FAILURE("unable to set priority", t);
        }
    }

    /**
     * deploy zimlet to a node by providing adminURL and uploadURL or deploy to all remote nodes
     */
    private static void deployZimletRemotely(String zimletFile, String adminURL, String uploadURL,
            boolean synchronous) throws ServiceException, IOException {
        File zf = new File(zimletFile);
        if (adminURL != null && uploadURL != null) {
            ZimletSoapUtil soapUtil = new ZimletSoapUtil(adminURL, uploadURL, null, null);
            soapUtil.deployZimletOnServer(zf.getName(), ByteUtil.getContent(zf), true);
        } else {
            //deploy to all servers except local
            ZimletSoapUtil soapUtil = null;
            List<Server> allServers = Provisioning.getInstance().getAllServers();
            for (Server server : allServers) {
                if (!server.isLocalServer() && canDeployZimlet(server)) {
                    if (soapUtil == null) {
                        soapUtil = new ZimletSoapUtil();
                        soapUtil.mSynchronous = synchronous;
                    }
                    soapUtil.deployZimletRemotely(server, zf.getName(), ByteUtil.getContent(zf), null, true);
                }
            }
        }
    }

    private static void undeployZimletRemotely(String zimlet) throws ServiceException, IOException {
        ZimletSoapUtil soapUtil = null;
        List<Server> allServers = Provisioning.getInstance().getAllServers();
        for (Server server : allServers) {
            if (!server.isLocalServer() && canDeployZimlet(server)) {
                if (soapUtil == null) {
                    soapUtil = new ZimletSoapUtil();
                }
                soapUtil.undeployZimletRemotely(server, zimlet);
            }
        }
    }

    private static boolean canDeployZimlet(Server server) {
        return server.hasMailboxService() || server.hasMailClientService() || server.hasZimletService()
                || server.hasAdminClientService() || server.hasWebClientService();
    }

    public static void showInfo(String zimlet) throws ServiceException, ZimletException, IOException {
        Provisioning prov = Provisioning.getInstance();
        Zimlet z = prov.getZimlet(zimlet);
        if (z == null) {
            throw AccountServiceException.NO_SUCH_ZIMLET(zimlet);
        }
        List<Zimlet> plist = orderZimletsByPriority();
        int pri = 0;
        for (Zimlet zp : plist) {
            if (zp.compareTo(z) == 0) {
                break;
            }
            pri++;
        }
        System.out.println("Zimlet " + z.getName());
        System.out.println("         Version: " + z.getAttr(Provisioning.A_zimbraZimletVersion));
        System.out.println("     Description: " + z.getDescription());
        System.out.println("        Priority: " + pri);
        System.out.println("         Enabled: " + (z.isEnabled() ? "true" : "false"));
        System.out.println("Indexing Enabled: " + (z.isIndexingEnabled() ? "true" : "false"));
        if (z.isExtension()) {
            System.out.println("       Extension: true");
        }
        String cosList = null;
        for (Cos cos : prov.getAllCos()) {
            for (String zc : getAvailableZimlets(cos).getZimletNamesAsArray()) {
                if (zc.compareTo(zimlet) != 0) {
                    continue;
                }
                if (cosList == null) {
                    cosList = cos.getName();
                } else {
                    cosList = cosList + ", " + cos.getName();
                }
                break;
            }
        }
        System.out.println("Activated in COS: " + cosList);
        System.out.println("          Config: " + z.getHandlerConfig());
        ZimletFile zf = getZimlet(zimlet);
        if (zf == null) {
            System.out.println("*** Zimlet file is missing on this machine");
        } else {
            ZimletDescription desc = zf.getZimletDescription();
            String val = desc.getRegexString();
            if (val != null) {
                System.out.println("           RegEx: " + val);
            }
            val = desc.getContentObjectAsXML();
            if (val != null) {
                System.out.println("  Content Object: " + val);
            }
            val = desc.getPanelItemAsXML();
            if (val != null) {
                System.out.println("      Panel Item: " + val);
            }
            val = null;
            for (String script : desc.getScripts()) {
                if (val == null) {
                    val = script;
                } else {
                    val = val + ", " + script;
                }
            }
            if (val != null) {
                System.out.println("         Scripts: " + val);
            }
            val = null;
            for (String css : desc.getStyleSheets()) {
                if (val == null) {
                    val = css;
                } else {
                    val = val + ", " + css;
                }
            }
            if (val != null) {
                System.out.println("             CSS: " + val);
            }
            val = null;
            for (String target : desc.getTargets()) {
                if (val == null) {
                    val = target;
                } else {
                    val = val + ", " + target;
                }
            }
            if (val != null) {
                System.out.println("         Targets: " + val);
            }
        }
    }

    public static void createZip(String dirName, String descFile) throws IOException {
        File dir = new File(dirName);
        if (!dir.exists() || !dir.isDirectory()) {
            throw new IOException("directory does not exist: " + dirName);
        }
        String target = descFile;
        boolean found = false;
        for (String f : dir.list()) {
            if (target != null) {
                if (target.compareTo(f) == 0) {
                    found = true;
                    break;
                }
            } else if (f.endsWith(".xml") && f.substring(0, f.length() - 4).compareTo(dir.getName()) == 0) {
                target = f;
                found = true;
                break;
            }
        }
        if (!found) {
            throw new IOException("Zimlet description not found, or not named correctly.");
        }
        String manifest = "Manifest-Version: 1.0\nZimlet-Description-File: " + target + "\n";
        JarOutputStream out = new JarOutputStream(
                new FileOutputStream(target.substring(0, target.length() - 4) + ".zip"),
                new Manifest(new ByteArrayInputStream(manifest.getBytes("UTF-8"))));
        for (File f : dir.listFiles()) {
            addZipEntry(out, f, null);
        }
        out.close();
    }

    private static void addZipEntry(ZipOutputStream out, File file, String path) throws IOException {
        String name = (path == null) ? file.getName() : path + "/" + file.getName();
        if (file.isDirectory()) {
            for (File f : file.listFiles()) {
                addZipEntry(out, f, name);
            }
            return;
        }
        ZipEntry entry = new ZipEntry(name);
        entry.setMethod(ZipEntry.STORED);
        entry.setSize(file.length());
        entry.setCompressedSize(file.length());
        entry.setCrc(computeCRC32(file));
        out.putNextEntry(entry);
        ByteUtil.copy(new FileInputStream(file), true, out, false);
        out.closeEntry();
    }

    private static long computeCRC32(File file) throws IOException {
        byte buf[] = new byte[32 * 1024];
        CRC32 crc = new CRC32();
        crc.reset();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            int bytesRead;
            while ((bytesRead = fis.read(buf)) != -1) {
                crc.update(buf, 0, bytesRead);
            }
            return crc.getValue();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                }
            }
        }
    }

    private static void test() {
        String ZIMLET_URL = "^/service/zimlet/([^/\\?]+)[/\\?]?.*$";
        String t1 = "/service/zimlet/po";
        String t2 = "/service/zimlet/foo?123";
        Pattern mPattern = Pattern.compile(ZIMLET_URL);
        Matcher matcher = mPattern.matcher(t1);
        if (matcher.matches()) {
            System.out.println(matcher.group(1));
        }
        matcher = mPattern.matcher(t2);
        if (matcher.matches()) {
            System.out.println(matcher.group(1));
        }
    }

    public static class ZimletSoapUtil {
        private String mUsername;
        private String mPassword;
        private String mAttachmentId;
        private String mAdminURL;
        private String mUploadURL;
        private ZAuthToken mAuth;
        private SoapHttpTransport mTransport;
        private boolean mSynchronous;
        private String mStatus;

        public ZimletSoapUtil() throws ServiceException {
            initZimletSoapUtil(null, null);
        }

        public ZimletSoapUtil(String adminURL, String uploadURL, String username, String password)
                throws ServiceException {
            mAdminURL = adminURL;
            mUploadURL = uploadURL;
            initZimletSoapUtil(username, password);
        }

        private void initZimletSoapUtil(String username, String password) throws ServiceException {
            mUsername = username != null ? username : LC.zimbra_ldap_user.value();
            mPassword = password != null ? password : LC.zimbra_ldap_password.value();
            mAuth = null;
            String adminURL = mAdminURL;
            if (adminURL == null) {
                String server = LC.zimbra_zmprov_default_soap_server.value();
                adminURL = URLUtil.getAdminURL(server);
            }
            SoapProvisioning sp = new SoapProvisioning();
            sp.soapSetURI(adminURL);
            sp.soapAdminAuthenticate(mUsername, mPassword);
        }

        public ZimletSoapUtil(ZAuthToken auth) {
            mAuth = auth;
        }

        public void deployZimletRemotely(Server server, String zimlet, byte[] data, DeployListener listener,
                boolean flushCache) throws ServiceException, IOException {
            if (server.hasMailClientService()) {
                ZimbraLog.zimlet.info("Deploying on service node %s", server.getName());
                deployZimletOnServiceNode(zimlet, data, server, listener, flushCache);
            } else {
                ZimbraLog.zimlet.info("Deploying on ui node %s", server.getName());
                deployZimletOnUiNode(zimlet, data, server, listener, flushCache);
            }
        }

        public void undeployZimletRemotely(Server server, String zimlet) throws ServiceException, IOException {
            ZimbraLog.zimlet.info("Undeploying on %s", server.getName());
            if (server.hasMailClientService()) {
                undeployZimletOnServiceNode(server, zimlet);
            } else {
                undeployZimletOnUiNode(server, zimlet);
            }
        }

        public void deployZimletOnServer(String zimlet, byte[] data, boolean flushCache) throws ServiceException {
            mTransport = null;
            try {
                mTransport = new SoapHttpTransport(mAdminURL);
                auth();
                mTransport.setAuthToken(mAuth);
                URL url = new URL(mUploadURL);
                mAttachmentId = postAttachment(mUploadURL, zimlet, data, url.getHost());

                soapDeployZimlet(flushCache);
                if (mSynchronous) {
                    ZimbraLog.zimlet.info("Deploy status: %s", mStatus);
                } else {
                    ZimbraLog.zimlet.info("Deploy initiated. Check the server's mailbox.log for the status.");
                }
            } catch (Exception e) {
                ZimbraLog.zimlet.info("deploy failed on %s", mAdminURL, e);
                if (e instanceof ServiceException) {
                    throw (ServiceException) e;
                } else {
                    throw ServiceException.FAILURE("Unable to deploy Zimlet " + zimlet + " on " + mAdminURL, e);
                }
            } finally {
                if (mTransport != null) {
                    mTransport.shutdown();
                }
            }
        }

        public void deployZimletOnServiceNode(String zimlet, byte[] data, Server server, DeployListener listener,
                boolean flushCache) throws ServiceException {
            mTransport = null;
            try {
                String adminUrl = URLUtil.getAdminURL(server, AdminConstants.ADMIN_SERVICE_URI);
                mTransport = new SoapHttpTransport(adminUrl);
                // auth if necessary
                if (mAuth == null) {
                    auth();
                }
                mTransport.setAuthToken(mAuth);

                // upload
                String uploadUrl = URLUtil.getAdminURL(server, "/service/upload?fmt=raw");
                ZimbraLog.zimlet.info("post to server %s, data size %d", server.getName(), data.length);
                mAttachmentId = postAttachment(uploadUrl, zimlet, data, server.getName());

                // deploy
                soapDeployZimlet(flushCache);
                if (mSynchronous) {
                    ZimbraLog.zimlet.info("Deploy status: %s", mStatus);
                } else {
                    ZimbraLog.zimlet.info("Deploy initiated. Check the server %s's mailbox.log for the status.",
                            server.getName());
                }
                if (listener != null) {
                    listener.markFinished(server);
                }
            } catch (Exception e) {
                ZimbraLog.zimlet.info("deploy failed on service node %s", server.getName(), e);
                if (listener != null) {
                    listener.markFailed(server, e);
                } else if (e instanceof ServiceException) {
                    throw (ServiceException) e;
                } else {
                    throw ServiceException.FAILURE("Unable to deploy Zimlet " + zimlet + " on " + server.getName(),
                            e);
                }
            } finally {
                if (mTransport != null) {
                    mTransport.shutdown();
                }
            }
        }

        void deployZimletOnUiNode(String zimlet, byte[] data, Server server, DeployListener listener,
                boolean flushCache) throws ServiceException {
            try {
                // auth if necessary
                if (mAuth == null) {
                    auth();
                }
                WebClientServiceUtil.sendDeployZimletRequestToUiNode(server, zimlet, mAuth.getValue(), data);
                if (mSynchronous) {
                    ZimbraLog.zimlet.info("Deploy status: %s", mStatus);
                } else {
                    ZimbraLog.zimlet.info("Deploy initiated. Check the server %s's mailbox.log for the status.",
                            server.getName());
                }
                if (listener != null) {
                    listener.markFinished(server);
                }
            } catch (Exception e) {
                ZimbraLog.zimlet.info("deploy failed on ui node %s", server.getName(), e);
                if (listener != null) {
                    listener.markFailed(server, e);
                } else if (e instanceof ServiceException) {
                    throw (ServiceException) e;
                } else {
                    throw ServiceException.FAILURE("Unable to deploy Zimlet " + zimlet + " on " + server.getName(),
                            e);
                }
            }
        }

        private void soapDeployZimlet(boolean flushCache) throws ServiceException, IOException {
            XMLElement req = new XMLElement(AdminConstants.DEPLOY_ZIMLET_REQUEST);
            req.addAttribute(AdminConstants.A_ACTION, AdminConstants.A_DEPLOYLOCAL);
            req.addAttribute(AdminConstants.A_FLUSH, flushCache);
            if (mSynchronous) {
                req.addAttribute(AdminConstants.A_SYNCHRONOUS, mSynchronous);
            }
            req.addElement(MailConstants.E_CONTENT).addAttribute(MailConstants.A_ATTACHMENT_ID, mAttachmentId);
            Element res = mTransport.invoke(req);
            if (mSynchronous) {
                mStatus = res.getElement(AdminConstants.E_PROGRESS).getAttribute(AdminConstants.A_STATUS, "");
            }
        }

        public void undeployZimletOnServiceNode(Server server, String zimlet) throws ServiceException {
            mTransport = null;
            try {
                String adminUrl = URLUtil.getAdminURL(server, AdminConstants.ADMIN_SERVICE_URI);
                mTransport = new SoapHttpTransport(adminUrl);

                // auth if necessary
                if (mAuth == null) {
                    auth();
                }
                mTransport.setAuthToken(mAuth);
                XMLElement req = new XMLElement(AdminConstants.UNDEPLOY_ZIMLET_REQUEST);
                req.addAttribute(AdminConstants.A_ACTION, AdminConstants.A_DEPLOYLOCAL);
                req.addAttribute(AdminConstants.A_NAME, zimlet);
                mTransport.invoke(req);
                ZimbraLog.zimlet.info("Undeploy initiated. Check the server %s's mailbox.log for the status.",
                        server.getName());
            } catch (Exception e) {
                if (e instanceof ServiceException) {
                    throw (ServiceException) e;
                } else {
                    throw ServiceException
                            .FAILURE("Unable to undeploy Zimlet " + zimlet + " on " + server.getName(), e);
                }
            } finally {
                if (mTransport != null) {
                    mTransport.shutdown();
                }
            }
        }

        public void undeployZimletOnUiNode(Server server, String zimlet) throws ServiceException {
            try {
                // auth if necessary
                if (mAuth == null) {
                    auth();
                }
                WebClientServiceUtil.sendUndeployZimletRequestToUiNode(server, zimlet, mAuth.getValue());
            } catch (Exception e) {
                ZimbraLog.zimlet.warn("undeployment failed on ui node %s", server.getName(), e);
                if (e instanceof ServiceException) {
                    throw (ServiceException) e;
                } else {
                    throw ServiceException
                            .FAILURE("Unable to undeploy Zimlet " + zimlet + " on " + server.getName(), e);
                }
            }
        }

        private String postAttachment(String uploadURL, String name, byte[] data, String domain)
                throws IOException, ZimletException {
            String aid = null;

            // no need/point to use ZimbraHttpConnectionManager because all callsites
            // of this methods are from command line, our idle connection reaper is
            // only started in the server anyway.  After the CLI exits all connections
            // will be released.
            HttpClient client = new HttpClient(); // CLI only, don't need conn mgr
            Map<String, String> cookieMap = mAuth.cookieMap(true);
            if (cookieMap != null) {
                HttpState state = new HttpState();
                for (Map.Entry<String, String> ck : cookieMap.entrySet()) {
                    state.addCookie(new org.apache.commons.httpclient.Cookie(domain, ck.getKey(), ck.getValue(),
                            "/", -1, false));
                }
                client.setState(state);
                client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
            }

            PostMethod post = new PostMethod(uploadURL);
            post.getParams().setSoTimeout(
                    (int) TimeUnit.MILLISECONDS.convert(LC.zimlet_deploy_timeout.intValue(), TimeUnit.SECONDS));
            int statusCode = -1;
            try {
                String contentType = URLConnection.getFileNameMap().getContentTypeFor(name);
                Part[] parts = { new ByteArrayPart(data, name, contentType) };
                post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
                statusCode = HttpClientUtil.executeMethod(client, post);

                if (statusCode == 200) {
                    String response = post.getResponseBodyAsString();
                    // "raw" response should be of the format
                    //   200,'null','aac04dac-b3c8-4c26-b9c2-c2534f1d6ba1:79d2722f-d4c7-4304-9961-e7bcb146fc32'
                    String[] responseParts = response.split(",", 3);
                    if (responseParts.length == 3) {
                        aid = responseParts[2].trim();
                        if (aid.startsWith("'") || aid.startsWith("\"")) {
                            aid = aid.substring(1);
                        }
                        if (aid.endsWith("'") || aid.endsWith("\"")) {
                            aid = aid.substring(0, aid.length() - 1);
                        }
                    }
                    if (aid == null) {
                        throw ZimletException
                                .CANNOT_DEPLOY("Attachment post failed, unexpected response: " + response, null);
                    }
                } else {
                    throw ZimletException.CANNOT_DEPLOY("Attachment post failed, status=" + statusCode, null);
                }
            } finally {
                post.releaseConnection();
            }

            return aid;
        }

        private void auth() throws ServiceException, IOException {
            XMLElement req = new XMLElement(AdminConstants.AUTH_REQUEST);
            req.addElement(AdminConstants.E_NAME).setText(mUsername);
            req.addElement(AdminConstants.E_PASSWORD).setText(mPassword);
            if (mTransport == null) {
                String adminUrl = URLUtil.getAdminURL(LC.zimbra_zmprov_default_soap_server.value());
                mTransport = new SoapHttpTransport(adminUrl);
            }
            Element resp = mTransport.invoke(req);
            mAuth = new ZAuthToken(resp.getElement(AccountConstants.E_AUTH_TOKEN), true);
        }

        private static class ByteArrayPart extends PartBase {
            private byte[] mData;
            private String mName;

            public ByteArrayPart(byte[] data, String name, String type) throws IOException {
                super(name, type, "UTF-8", "binary");
                mName = name;
                mData = data;
            }

            @Override
            protected void sendData(OutputStream out) throws IOException {
                out.write(mData);
            }

            @Override
            protected long lengthOfData() throws IOException {
                return mData.length;
            }

            @Override
            protected void sendDispositionHeader(OutputStream out) throws IOException {
                super.sendDispositionHeader(out);
                StringBuilder buf = new StringBuilder();
                buf.append("; filename=\"").append(mName).append("\"");
                out.write(buf.toString().getBytes());
            }
        }
    }

    private static final int INSTALL_ZIMLET = 10;
    private static final int UNINSTALL_ZIMLET = 11;
    private static final int LIST_ZIMLETS = 12;
    private static final int ACL_ZIMLET = 13;
    private static final int LIST_ACLS = 14;
    private static final int DUMP_CONFIG = 15;
    private static final int INSTALL_CONFIG = 16;
    private static final int LDAP_DEPLOY = 17;
    private static final int DEPLOY_ZIMLET = 18;
    private static final int ENABLE_ZIMLET = 19;
    private static final int DISABLE_ZIMLET = 20;
    private static final int LIST_PRIORITY = 21;
    private static final int SET_PRIORITY = 22;
    private static final int INFO = 23;
    private static final int CREATE_ZIP = 24;
    private static final int TEST = 99;

    private static final String INSTALL_CMD = "install";
    private static final String UNINSTALL_CMD = "uninstall";
    private static final String UNDEPLOY_CMD = "undeploy";
    private static final String LIST_CMD = "listzimlets";
    private static final String ACL_CMD = "acl";
    private static final String LIST_ACLS_CMD = "listacls";
    private static final String DUMP_CONFIG_CMD = "getconfigtemplate";
    private static final String INSTALL_CONFIG_CMD = "configure";
    private static final String LDAP_DEPLOY_CMD = "ldapdeploy";
    private static final String DEPLOY_CMD = "deploy";
    private static final String ENABLE_CMD = "enable";
    private static final String DISABLE_CMD = "disable";
    private static final String LIST_PRIORITY_CMD = "listpriority";
    private static final String SET_PRIORITY_CMD = "setpriority";
    private static final String INFO_CMD = "info";
    private static final String CREATE_ZIP_CMD = "createzip";
    private static final String TEST_CMD = "test";

    private static Map<String, Integer> mCommands;

    private static void addCommand(String cmd, int cmdId) {
        mCommands.put(cmd, Integer.valueOf(cmdId));
    }

    private static void setup() {
        mCommands = new HashMap<String, Integer>();
        addCommand(DEPLOY_CMD, DEPLOY_ZIMLET);
        addCommand(UNDEPLOY_CMD, UNINSTALL_ZIMLET);
        addCommand(INSTALL_CMD, INSTALL_ZIMLET);
        addCommand(UNINSTALL_CMD, UNINSTALL_ZIMLET);
        addCommand(LIST_CMD, LIST_ZIMLETS);
        addCommand(ACL_CMD, ACL_ZIMLET);
        addCommand(LIST_ACLS_CMD, LIST_ACLS);
        addCommand(DUMP_CONFIG_CMD, DUMP_CONFIG);
        addCommand(INSTALL_CONFIG_CMD, INSTALL_CONFIG);
        addCommand(LDAP_DEPLOY_CMD, LDAP_DEPLOY);
        addCommand(ENABLE_CMD, ENABLE_ZIMLET);
        addCommand(DISABLE_CMD, DISABLE_ZIMLET);
        addCommand(LIST_PRIORITY_CMD, LIST_PRIORITY);
        addCommand(SET_PRIORITY_CMD, SET_PRIORITY);
        addCommand(INFO_CMD, INFO);
        addCommand(CREATE_ZIP_CMD, CREATE_ZIP);
        addCommand(TEST_CMD, TEST);
    }

    private static void usage() {
        System.out.println(
                "Usage: zmzimletctl [-l] [-a <admin url> -u <upload url>] [command] [ zimlet.zip | config.xml | zimlet ]");
        System.out.println(
                "\tdeploy {zimlet.zip} - install, ldapDeploy, grant ACL on default COS, then enable Zimlet");
        System.out.println("\tundeploy {zimlet} - remove the Zimlet from the system");
        System.out.println("\tinstall {zimlet.zip} - installs the Zimlet files on this host");
        System.out.println("\tldapDeploy {zimlet} - add the Zimlet entry to the system");
        System.out.println("\tenable {zimlet} - enables the Zimlet");
        System.out.println("\tdisable {zimlet} - disables the Zimlet");
        System.out.println(
                "\tacl {zimlet} {cos1} grant|deny [{cos2} grant|deny...] - change the ACL for the Zimlet on a COS");
        System.out.println("\tlistAcls {zimlet} - list ACLs for the Zimlet");
        System.out.println("\tlistZimlets - show status of all the Zimlets in the system.");
        System.out.println("\tgetConfigTemplate {zimlet.zip} - dumps the configuration");
        System.out.println("\tconfigure {config.xml} - installs the configuration");
        System.out.println("\tlistPriority - show the current Zimlet priorities (0 high, 9 low)");
        System.out.println("\tsetPriority {zimlet} {priority} - set Zimlet priority");
        System.out.println("\tinfo {zimlet} - show information about zimlet");
        System.out.println(
                "\tcreateZip {zimlet directory} [description-file] - creates zimlet.zip from the contents in the directory");
        System.exit(1);
    }

    private static int lookupCmd(String cmd) {
        Integer i = mCommands.get(cmd.toLowerCase());
        if (i == null) {
            usage();
        }
        return i.intValue();
    }

    private static void dispatch(String[] args) {
        boolean isLocalInstall = false;
        try {
            if (args[argPos].equals("-l")) {
                isLocalInstall = true;
                argPos++;
            }
            String adminURL = null;
            String uploadURL = null;
            if (args[argPos].equals("-a")) {
                adminURL = args[++argPos];
                argPos++;
            }
            if (args[argPos].equals("-u")) {
                uploadURL = args[++argPos];
                argPos++;
            }
            if (argPos >= args.length) {
                usage();
            }

            int cmd = lookupCmd(args[argPos++]);
            switch (cmd) {
            case LIST_ZIMLETS:
                boolean everything = false;
                if (args.length > argPos && args[argPos].equals("all")) {
                    everything = true;
                }
                listAllZimlets(everything);
                System.exit(0);
            case LIST_PRIORITY:
                listPriority();
                System.exit(0);
            case TEST:
                test();
                System.exit(0);
            }

            if (args.length < argPos + 1) {
                usage();
            }
            String zimlet = args[argPos++];
            switch (cmd) {
            case DEPLOY_ZIMLET:
                deployZimletLocally(new ZimletFile(zimlet));
                if (!isLocalInstall) {
                    boolean synchronous = false;
                    if (args.length > argPos && args[argPos].equals("sync")) {
                        synchronous = true;
                    }
                    deployZimletRemotely(zimlet, adminURL, uploadURL, synchronous);
                }
                break;
            case INSTALL_ZIMLET:
                installZimletLocally(new ZimletFile(zimlet));
                break;
            case UNINSTALL_ZIMLET:
                undeployZimletLocally(zimlet);
                if (!isLocalInstall) {
                    undeployZimletRemotely(zimlet);
                }
                break;
            case LDAP_DEPLOY:
                ldapDeploy(zimlet);
                break;
            case ACL_ZIMLET:
                if (args.length < (argPos + 2) || args.length % 2 != 0) {
                    usage();
                }
                aclZimlet(zimlet, args);
                break;
            case SET_PRIORITY:
                if (args.length < (argPos + 1)) {
                    usage();
                }
                setPriority(zimlet, Integer.parseInt(args[argPos]));
                listPriority();
                break;
            case LIST_ACLS:
                listAcls(zimlet);
                break;
            case ENABLE_ZIMLET:
                enableZimlet(zimlet);
                break;
            case DISABLE_ZIMLET:
                disableZimlet(zimlet);
                break;
            case DUMP_CONFIG:
                dumpConfig(zimlet);
                break;
            case INSTALL_CONFIG:
                installConfig(new String(ByteUtil.getContent(new File(zimlet))));
                break;
            case INFO:
                showInfo(zimlet);
                break;
            case CREATE_ZIP:
                createZip(zimlet, (args.length == argPos) ? null : args[argPos]);
                break;
            default:
                usage();
                break;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            usage();
        } catch (Exception e) {
            e.printStackTrace();
            if (sQuietMode) {
                ZimbraLog.zimlet.error("Error " + e.getMessage());
            } else {
                ZimbraLog.zimlet.error("Error", e);
            }
            System.exit(1);
        }
    }

    private static boolean sQuietMode = false;
    private static int argPos = 0;

    private static void getOpt(String[] args) {
        if (args.length < 1) {
            usage();
        }

        int index = 0;
        while (index < args.length) {
            if (args[index].equals("-q")) {
                sQuietMode = true;
            } else {
                break;
            }
            index++;
        }
        argPos = index;
    }

    public static void main(String[] args) throws IOException {

        getOpt(args);
        if (sQuietMode) {
            CliUtil.toolSetup("WARN");
        } else {
            CliUtil.toolSetup();
        }
        SoapTransport.setDefaultUserAgent("zmzimletctl", BuildInfo.VERSION);
        setup();
        dispatch(args);
    }
}