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