com.appcelerator.titanium.desktop.ui.wizard.Packager.java Source code

Java tutorial

Introduction

Here is the source code for com.appcelerator.titanium.desktop.ui.wizard.Packager.java

Source

/**
 * Copyright 2011-2012 Appcelerator, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.appcelerator.titanium.desktop.ui.wizard;

import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import com.appcelerator.titanium.core.TitaniumConstants;
import com.appcelerator.titanium.core.TitaniumCorePlugin;
import com.appcelerator.titanium.core.user.ITitaniumUser;
import com.appcelerator.titanium.desktop.DesktopPlugin;
import com.appcelerator.titanium.desktop.DesktopUsageUtil;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.EclipseUtil;
import com.aptana.core.util.FileUtil;
import com.aptana.core.util.IOUtil;
import com.aptana.core.util.StringUtil;
import com.aptana.ui.util.UIUtils;

public class Packager {

    public static final String WINDOWS_PLATFORM = TiManifest.WINDOWS_PLATFORM;
    public static final String LINUX_PLATFORM = TiManifest.LINUX_PLATFORM;
    public static final String MAC_PLATFORM = TiManifest.MAC_PLATFORM;

    /**
     * Constants used in timanifest FIXME Use enums for these?
     */
    public static final String PUBLIC_VISIBILITY = "public"; //$NON-NLS-1$
    public static final String PRIVATE_VISIBILITY = "private"; //$NON-NLS-1$
    public static final String NETWORK_RUNTIME = "network"; //$NON-NLS-1$
    public static final String INCLUDE_RUNTIME = "include"; //$NON-NLS-1$

    private static final String ENCODING = "UTF-8"; //$NON-NLS-1$
    private static final String PUBLISH_URL = "https://api.appcelerator.net/p/v1/publish"; //$NON-NLS-1$
    private static final String PUBLISH_STATUS_URL = "https://api.appcelerator.net/p/v1/publish-status"; //$NON-NLS-1$

    /**
     * Helper function to format a url
     * 
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     */
    private URL makeURL(String baseURL, Map<String, String> params)
            throws UnsupportedEncodingException, MalformedURLException {
        if (params != null && !params.isEmpty()) {
            return new URL(baseURL + '?' + getQueryString(params));
        }
        return new URL(baseURL);
    }

    /**
     * Show the webpage that lists the last releases generated by Desktop packaging for a given project.
     * 
     * @param project
     */
    public void showPackages(final IProject project) {
        final List<Release> releases = Release.load(project);

        UIUtils.getDisplay().asyncExec(new Runnable() {
            // For now let's just pop open a dialog with the releases
            public void run() {
                try {
                    InputStream in = FileLocator.openStream(DesktopPlugin.getDefault().getBundle(),
                            Path.fromPortableString("template.html"), false); //$NON-NLS-1$
                    String html = IOUtil.read(in);

                    StringBuilder builder = new StringBuilder();
                    String appPage = StringUtil.EMPTY;
                    String pubDate = StringUtil.EMPTY;
                    if (releases != null) {
                        for (Release release : releases) {
                            String label = release.getLabel();
                            String url = release.getURL();
                            String platform = release.getPlatform();

                            builder.append("<div class=\"row even\">\n"); //$NON-NLS-1$
                            builder.append(
                                    "<div class=\"platform\"><img height=\"20\" width=\"20\" src=\"http://studio-titanium.s3.amazonaws.com/") //$NON-NLS-1$
                                    .append(platform).append("_small.png\"/></div>\n"); //$NON-NLS-1$
                            builder.append("<div class=\"label\">").append(label).append("</div>\n"); //$NON-NLS-1$ //$NON-NLS-2$
                            builder.append("<div class=\"link\"><a href=\"").append(url).append("\">").append(url) //$NON-NLS-1$ //$NON-NLS-2$
                                    .append("</a></div>\n</div>\n"); //$NON-NLS-1$

                            appPage = release.getAppPage();
                            pubDate = release.getPubDate();
                        }
                    }
                    // We need to inject the releases into the list
                    html = html.replaceAll(Pattern.quote("${RELEASES}"), builder.toString()); //$NON-NLS-1$
                    html = html.replaceAll(Pattern.quote("${APP_PAGE}"), appPage); //$NON-NLS-1$
                    html = html.replaceAll(Pattern.quote("${PUB_DATE}"), pubDate); //$NON-NLS-1$

                    // if tehre are no releases, show the no links div in the webpage
                    html = html.replaceAll(Pattern.quote("${DISPLAY_LINKS}"), //$NON-NLS-1$
                            (releases == null || releases.isEmpty()) ? "none" : "block"); //$NON-NLS-1$ //$NON-NLS-2$
                    html = html.replaceAll(Pattern.quote("${DISPLAY_NO_LINKS}"), //$NON-NLS-1$
                            (releases == null || releases.isEmpty()) ? "block" : "none"); //$NON-NLS-1$ //$NON-NLS-2$

                    File tmpFile = File.createTempFile("releases", ".html"); //$NON-NLS-1$ //$NON-NLS-2$
                    FileWriter writer = new FileWriter(tmpFile);
                    writer.write(html);
                    writer.close();

                    try {
                        PlatformUI.getWorkbench().getBrowserSupport()
                                .createBrowser(IWorkbenchBrowserSupport.AS_EDITOR, null,
                                        Messages.Packager_LastPackagedDist_Title,
                                        Messages.Packager_LastPackagedDist_ToolTip)
                                .openURL(new URL(tmpFile.toURI().toURL().toString()));
                    } catch (Exception e) {
                        IdeLog.logError(DesktopPlugin.getDefault(), "Unable to open last packaged distribution", e); //$NON-NLS-1$
                    }
                } catch (Exception e) {
                    MessageDialog.openError(new Shell(), Messages.Packager_LinksWebpageOpenError, e.getMessage());
                }
            }
        });
    }

    /**
     * Package up the desktop app, push it to the webservice, poll the status of packaging remotely, then store the
     * resulting releases in prefs for the project and pop open the webpage showing links to the generated releases.
     * 
     * @param project
     * @param platforms
     * @param runtime
     * @param release
     * @param visibility
     * @param monitor
     * @return
     */
    public IStatus distribute(IProject project, Set<String> platforms, String runtime, boolean release,
            String visibility, boolean showSplash, IProgressMonitor monitor) {
        // set packaging message
        SubMonitor sub = SubMonitor.convert(monitor, Messages.Packager_PackagingTaskName, 1100);

        File zipFile = null;
        IPath destDir = null;
        try {
            // if offline, don't attempt
            if (!isOnline()) {
                return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, "{'offline':true}"); //$NON-NLS-1$
            }

            // make sure required files/dirs are present
            File resources = project.getLocation().append("Resources").toFile(); //$NON-NLS-1$
            if (!resources.exists()) {
                return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID,
                        Messages.Packager_NoResourcesFolderValidationError);
            }
            File tiapp = project.getLocation().append("tiapp.xml").toFile(); //$NON-NLS-1$
            if (!tiapp.exists()) {
                return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID,
                        Messages.Packager_NoTIAPPXMLValidationError);
            }
            sub.worked(25);

            // write out timanifest
            TiManifest manifest = new TiManifest(project);
            manifest.write(platforms, runtime, release, visibility, showSplash, sub.newChild(50));

            // copy files to be published
            destDir = copyAppFiles(project, sub.newChild(250));

            // Zip destDir
            zipFile = File.createTempFile(project.getName(), ".zip"); //$NON-NLS-1$
            new DirectoryZipper().createZip(destDir.toFile(), zipFile, sub.newChild(200));

            // Push it up!
            publish(project, zipFile);
            sub.worked(500);

            // Send Analytics event
            DesktopUsageUtil.sendPackageEvent(platforms.contains(TiManifest.WINDOWS_PLATFORM),
                    platforms.contains(TiManifest.LINUX_PLATFORM), platforms.contains(TiManifest.MAC_PLATFORM),
                    manifest.getGUID());
            sub.worked(75);
        } catch (CoreException e) {
            return e.getStatus();
        } catch (Exception e) {
            return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, 0, e.getMessage(), e);
        } finally {
            if (destDir != null) {
                FileUtil.deleteRecursively(destDir.toFile());
            }
            if (zipFile != null) {
                if (!zipFile.delete()) {
                    zipFile.deleteOnExit();
                }
            }
            sub.done();
        }
        return Status.OK_STATUS;
    }

    private void publish(IProject project, File zipFile) throws UnsupportedEncodingException, MalformedURLException,
            IOException, ProtocolException, FileNotFoundException, ParseException, CoreException {
        // FIXME What if user isn't signed in? We can enforce that they must be in enablement of action, but maybe the
        // sign-in timed out or something.
        ITitaniumUser user = TitaniumCorePlugin.getDefault().getUserManager().getSignedInUser();
        Map<String, String> data = new HashMap<String, String>();
        data.put("sid", user.getSessionID()); //$NON-NLS-1$
        data.put("token", user.getToken()); //$NON-NLS-1$
        data.put("uid", user.getUID()); //$NON-NLS-1$
        data.put("uidt", user.getUIDT()); //$NON-NLS-1$

        // Post zip to url
        URL postURL = makeURL(PUBLISH_URL, data);

        IdeLog.logInfo(DesktopPlugin.getDefault(), MessageFormat.format("API Request: {0}", postURL), //$NON-NLS-1$
                com.appcelerator.titanium.core.IDebugScopes.API);

        HttpURLConnection con = (HttpURLConnection) postURL.openConnection();
        con.setUseCaches(false);
        con.setDoOutput(true);
        con.setRequestMethod("POST"); //$NON-NLS-1$
        con.setRequestProperty("User-Agent", TitaniumConstants.USER_AGENT); //$NON-NLS-1$
        // Pipe zip contents to stream
        OutputStream out = con.getOutputStream();
        IOUtil.pipe(new BufferedInputStream(new FileInputStream(zipFile)), out);
        out.flush();
        out.close();

        // Force to finish, grab response code
        int code = con.getResponseCode();

        // Delete the destDir after POST has finished
        if (!zipFile.delete()) {
            zipFile.deleteOnExit();
        }

        // If the http code is 200 from POST, parse response as JSON. Else display error
        if (code == 200) {
            String responseText = IOUtil.read(con.getInputStream());
            IdeLog.logInfo(DesktopPlugin.getDefault(),
                    MessageFormat.format("API Response: {0}:{1}", code, responseText), //$NON-NLS-1$
                    com.appcelerator.titanium.core.IDebugScopes.API);

            Object result = new JSONParser().parse(responseText);

            if (result instanceof JSONObject) {
                JSONObject json = (JSONObject) result;
                boolean successful = (Boolean) json.get("success"); //$NON-NLS-1$
                if (!successful) {
                    // FIXME For some reason we're failing here but Titanium Developer isn't. Bad zip file? Maybe we're
                    // piping it to stream incorrectly?
                    throw new CoreException(new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, code,
                            (String) json.get("message"), null)); //$NON-NLS-1$
                }
                // run pollPackagingRequest with "ticket" from JSON and project GUID if 200 and reports success
                pollPackagingRequest(project, (String) json.get("ticket")); //$NON-NLS-1$
            }
        } else {
            String responseText = IOUtil.read(con.getErrorStream());
            IdeLog.logError(DesktopPlugin.getDefault(),
                    MessageFormat.format("API Response: {0}:{1}.", code, responseText), //$NON-NLS-1$
                    com.appcelerator.titanium.core.IDebugScopes.API);

            throw new CoreException(new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, code,
                    Messages.Packager_PackagingFailedHTTPError + code, null));
        }
    }

    private void pollPackagingRequest(final IProject project, final String ticket) throws CoreException {
        Job pollingJob = new Job(Messages.Packager_PollingPackageStatusTaskName) {

            @Override
            protected IStatus run(IProgressMonitor monitor) {
                monitor.beginTask(Messages.Packager_PollingPackageStatusTaskName, -1);
                try {
                    while (true) {
                        Map<String, String> data = new HashMap<String, String>();
                        data.put("ticket", ticket); //$NON-NLS-1$

                        IStatus result = invokeCloudService(new URL(PUBLISH_STATUS_URL), data, "POST"); //$NON-NLS-1$
                        if (!result.isOK()) {
                            return result;
                        }

                        String rawJSON = result.getMessage();
                        Object parsed = new JSONParser().parse(rawJSON);
                        JSONObject json = (JSONObject) parsed;

                        if ("complete".equals(json.get("status"))) //$NON-NLS-1$//$NON-NLS-2$
                        {
                            Release.updateForProject(project, json);
                            // We don't show packages in UI when we are testing
                            if (!EclipseUtil.isTesting()) {
                                showPackages(project);
                            }
                            return Status.OK_STATUS;
                        } else if (json.containsKey("success") && !((Boolean) json.get("success"))) //$NON-NLS-1$ //$NON-NLS-2$
                        {
                            return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, 0,
                                    Messages.Packager_PackagingFailedError + json.get("message"), null); //$NON-NLS-1$
                        }
                        if (monitor != null && monitor.isCanceled()) {
                            return Status.CANCEL_STATUS;
                        }
                        // Retry in 10 seconds
                        Thread.sleep(10000);
                    }
                } catch (Exception e) {
                    return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, 0, e.getMessage(), e);
                }
            }
        };

        pollingJob.setUser(true);
        pollingJob.schedule();

        // Make this a blocking job for unit tests
        if (EclipseUtil.isTesting()) {
            try {
                pollingJob.join();
                if (pollingJob.getResult() != Status.OK_STATUS) {
                    throw new CoreException(new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, 0,
                            "Polling Package Request Failed:" + pollingJob.getResult().getMessage(), null)); //$NON-NLS-1$
                }
            } catch (InterruptedException e) {
            }
        }
    }

    /**
     * Copy app files for packaging, return the destination directory
     */
    private IPath copyAppFiles(IProject project, IProgressMonitor monitor) throws CoreException {
        SubMonitor sub = SubMonitor.convert(monitor, 100);

        // Destination, create a temp dir to hold it
        File destDir = new File(System.getProperty("java.io.tmpdir"), project.getName()); //$NON-NLS-1$
        destDir.mkdirs();
        IFileStore destDirStore = EFS.getLocalFileSystem().fromLocalFile(destDir);

        IFileStore projectDirStore = EFS.getStore(project.getLocationURI());
        IFileStore resources = projectDirStore.getChild("Resources"); //$NON-NLS-1$
        IFileStore modules = projectDirStore.getChild("modules"); //$NON-NLS-1$
        IFileStore timanifest = projectDirStore.getChild("timanifest"); //$NON-NLS-1$
        IFileStore manifest = projectDirStore.getChild("manifest"); //$NON-NLS-1$
        IFileStore tiapp = projectDirStore.getChild("tiapp.xml"); //$NON-NLS-1$
        IFileStore changeLog = projectDirStore.getChild("CHANGELOG.txt"); //$NON-NLS-1$
        IFileStore license = projectDirStore.getChild("LICENSE.txt"); //$NON-NLS-1$

        List<IFileStore> fileArray = new ArrayList<IFileStore>();
        fileArray.add(tiapp);
        fileArray.add(timanifest);
        fileArray.add(manifest);
        if (changeLog.fetchInfo().exists()) {
            fileArray.add(changeLog);
        }
        if (license.fetchInfo().exists()) {
            fileArray.add(license);
        }
        // TODO Just create a list of strings, add to file array and copy as per middle, rather than treating Resources
        // or modules specially

        // copy Resources to temp dir
        resources.copy(destDirStore.getChild("Resources"), EFS.OVERWRITE, sub.newChild(60)); //$NON-NLS-1$

        // copy file array to destDir
        for (IFileStore fs : fileArray) {
            fs.copy(destDirStore.getChild(fs.getName()), EFS.OVERWRITE, sub.newChild(1));
        }

        // If there are modules, copy modules to destDir/modules
        if (modules.fetchInfo().exists()) {
            modules.copy(destDirStore.getChild("modules"), EFS.OVERWRITE, sub.newChild(25)); //$NON-NLS-1$
        }

        sub.done();
        return Path.fromOSString(destDir.getAbsolutePath());
    }

    // TODO Re-use this method whenever we need to hit URLs! This is very similar to TitaniumUser code!
    private IStatus invokeCloudService(URL baseURL, Map<String, String> data, String type) {
        DataOutputStream outputStream = null;
        try {

            if (type == null) {
                type = "POST"; //$NON-NLS-1$
            }

            if (data == null) {
                data = new HashMap<String, String>();
            }

            // always pass MID
            data.put("mid", TitaniumCorePlugin.getMID()); //$NON-NLS-1$
            ITitaniumUser user = TitaniumCorePlugin.getDefault().getUserManager().getSignedInUser();
            data.put("sid", user.getSessionID()); //$NON-NLS-1$
            data.put("token", user.getToken()); //$NON-NLS-1$
            data.put("uid", user.getUID()); //$NON-NLS-1$
            data.put("uidt", user.getUIDT()); //$NON-NLS-1$

            // If this is not a POST, append query params don't put as data in body!
            if (!"POST".equals(type)) //$NON-NLS-1$
            {
                baseURL = makeURL(baseURL.toString(), data);
            }
            HttpURLConnection con = (HttpURLConnection) baseURL.openConnection();
            con.setUseCaches(false);
            con.setRequestMethod(type);
            con.setRequestProperty("User-Agent", TitaniumConstants.USER_AGENT); //$NON-NLS-1$
            if ("POST".equals(type)) //$NON-NLS-1$
            {
                con.setDoOutput(true);
                // Write the data to the connection
                outputStream = new DataOutputStream(con.getOutputStream());
                outputStream.writeBytes(getQueryString(data));
                outputStream.flush();
            }

            IdeLog.logInfo(DesktopPlugin.getDefault(), MessageFormat.format("API Request: {0}", baseURL), //$NON-NLS-1$
                    com.appcelerator.titanium.core.IDebugScopes.API);

            // Force to finish, grab response code
            int code = con.getResponseCode();

            // If the http code is 200 from POST, parse response as JSON. Else display error
            if (code == 200) {
                String responseText = IOUtil.read(con.getInputStream());
                IdeLog.logInfo(DesktopPlugin.getDefault(),
                        MessageFormat.format("API Response: {0}:{1}", code, responseText), //$NON-NLS-1$
                        com.appcelerator.titanium.core.IDebugScopes.API);

                return new Status(IStatus.OK, DesktopPlugin.PLUGIN_ID, code, responseText, null);
            }
            // TODO Read from connection to populate message!
            return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, code, null, null);
        } catch (Exception e) {
            return new Status(IStatus.ERROR, DesktopPlugin.PLUGIN_ID, 0, e.getMessage(), e);
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    // ignores the exception
                }
            }
        }
    }

    private String getQueryString(Map<String, String> data) throws UnsupportedEncodingException {
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, String> entry : data.entrySet()) {
            builder.append(URLEncoder.encode(entry.getKey(), ENCODING)).append('=')
                    .append(URLEncoder.encode(entry.getValue(), ENCODING)).append('&');
        }
        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1); // chop off trailing '&'
        }
        return builder.toString();
    }

    private boolean isOnline() {
        if (DesktopPlugin.getDefault() != null) {
            ITitaniumUser user = TitaniumCorePlugin.getDefault().getUserManager().getSignedInUser();
            if (user != null && user.isOnline()) {
                return true;
            }
        }

        return false;
    }
}