package com.mobilesorcery.sdk.ui.targetphone.iphoneos; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.InetAddress; import java.net.URL; import java.net.URLEncoder; import java.text.DateFormat; import java.text.MessageFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.util.thread.QueuedThreadPool; import com.mobilesorcery.sdk.builder.iphoneos.PropertyInitializer; import com.mobilesorcery.sdk.core.CoreMoSyncPlugin; import com.mobilesorcery.sdk.core.DefaultPackager; import com.mobilesorcery.sdk.core.IBuildResult; import com.mobilesorcery.sdk.core.IBuildState; import com.mobilesorcery.sdk.core.IBuildVariant; import com.mobilesorcery.sdk.core.MoSyncBuilder; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.Util; import com.mobilesorcery.sdk.core.templates.Template; public class IPhoneOSOTAServer extends AbstractHandler { private static IPhoneOSOTAServer defaultServer; private Server server; private final IdentityHashMap<MoSyncProject, IBuildVariant> projects = new IdentityHashMap<MoSyncProject, IBuildVariant>(); private CopyOnWriteArrayList<IPhoneOSOTAServerListener> listeners = new CopyOnWriteArrayList<IPhoneOSOTAServerListener>(); public static IPhoneOSOTAServer getDefault() { if (defaultServer == null) { defaultServer = new IPhoneOSOTAServer(); } return defaultServer; } public void offerProject(MoSyncProject project, IBuildVariant variant) throws IOException { try { startServer(project, variant); } catch (Exception e) { throw new IOException("Could not start server", e); } } public void addListener(IPhoneOSOTAServerListener listener) { listeners.add(listener); } public void removeListener(IPhoneOSOTAServerListener listener) { listeners.remove(listener); } private synchronized void startServer(MoSyncProject project, IBuildVariant variant) throws Exception { if (projects.isEmpty()) { server = new Server(getPort()); server.setThreadPool(new QueuedThreadPool(5)); server.setHandler(this); Connector connector = new SelectChannelConnector(); connector.setPort(getPort()); connector.setMaxIdleTime(120000); server.setConnectors(new Connector[] { connector }); server.start(); } IBuildVariant prevVariant = projects.put(project, variant); if (prevVariant != null && CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("Warning: replaced variant for iOS OTA. {0}, {1}", project, variant); } } private synchronized void stopServer(MoSyncProject project) throws Exception { projects.remove(project); if (projects.isEmpty()) { server.stop(); } } private int getPort() throws IOException { return IPhoneOSTransportPlugin.getDefault().getServerURL().getPort(); } @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (target.startsWith("/")) { target = target.substring(1); } String ext = Util.getExtension(target); String[] segments = target.split("/"); String projectName = Util.getNameWithoutExtension(segments[0]); String projectNameWithExt = segments[0]; String fileName = segments[segments.length - 1]; if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("Device requested {0} for project {1} (extension {2})", target, projectName, ext); } if (Util.isEmpty(projectName)) { generateIndex(response); } else { IProject project = ResourcesPlugin.getPlugin().getWorkspace().getRoot().getProject(projectName); if (!project.exists()) { project = ResourcesPlugin.getPlugin().getWorkspace().getRoot().getProject(projectNameWithExt); } MoSyncProject mosyncProject = MoSyncProject.create(project); if (mosyncProject != null) { IBuildVariant variant = projects.get(mosyncProject); if (variant != null) { if ("plist".equals(ext)) { generatePlist(response, mosyncProject, variant); } else if ("mobileprovisioning".equals(ext)) { generateProvisioningFile(response, mosyncProject); } else if ("ipa".equals(ext)) { transferApp(response, mosyncProject, variant); } else if (ext.equals("png") && fileName.startsWith("icon")) { transferIcon(response, mosyncProject, variant); } else if (ext.equals("png") && fileName.startsWith("image")) { transferImage(response, mosyncProject, variant); } } } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } } private void transferImage(HttpServletResponse response, MoSyncProject mosyncProject, IBuildVariant variant) throws IOException { URL defaultImageFileURL = FileLocator.find(IPhoneOSTransportPlugin.getDefault().getBundle(), new Path("/images/default-itunes-image512x512.png"), null); URL defaultImageFile = FileLocator.toFileURL(defaultImageFileURL); if (defaultImageFile != null) { transferFile(response, new File(defaultImageFile.getPath())); } } private void transferApp(HttpServletResponse response, MoSyncProject mosyncProject, IBuildVariant variant) throws IOException { for (IPhoneOSOTAServerListener listener : listeners) { listener.appRequested(mosyncProject); } IBuildState buildState = mosyncProject.getBuildState(variant); List<File> ipaFile = buildState.getBuildResult().getBuildResult().get(IBuildResult.MAIN); transferFile(response, ipaFile.isEmpty() ? null : ipaFile.get(0)); } private void transferIcon(HttpServletResponse response, MoSyncProject mosyncProject, IBuildVariant variant) throws IOException { IPath output = MoSyncBuilder.getPackageOutputPath(mosyncProject.getWrappedProject(), variant); // We make use of internal knowledge! IPath iconFile = output.append(new Path("xcode-proj/Icon.png")); transferFile(response, iconFile.toFile()); } private void transferFile(HttpServletResponse response, File file) throws IOException { response.setContentType("application/octet-stream"); response.setStatus(HttpServletResponse.SC_OK); if (file != null && file.exists()) { FileInputStream input = new FileInputStream(file); Util.transfer(input, response.getOutputStream()); } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } Util.safeClose(response.getOutputStream()); } private void generateProvisioningFile(HttpServletResponse response, MoSyncProject project) throws IOException { response.setContentType("application/octet-stream"); response.setStatus(HttpServletResponse.SC_OK); String provisioningFile = project.getProperty(PropertyInitializer.IOS_PROVISIONING_FILE); File absoluteProvisioningFile = Util.relativeTo(project.getWrappedProject().getLocation().toFile(), provisioningFile); FileInputStream input = new FileInputStream(absoluteProvisioningFile); try { Util.transfer(input, response.getOutputStream()); } finally { Util.safeClose(input); Util.safeClose(response.getOutputStream()); } } private void generatePlist(HttpServletResponse response, MoSyncProject mosyncProject, IBuildVariant variant) throws IOException { response.setContentType("text/xml"); response.setCharacterEncoding("UTF-8"); response.setStatus(HttpServletResponse.SC_OK); Template plistTemplate = new Template(getClass().getResource("/templates/dist.plist.template")); Map<String, String> map = new HashMap<String, String>(); DefaultPackager dp = new DefaultPackager(mosyncProject, variant); map.putAll(dp.getParameters().toMap()); map.put("base-url", getBaseURL()); map.put("project-name", mosyncProject.getName()); map.put(DefaultPackager.APP_VENDOR_NAME_BUILD_PROP, mosyncProject.getProperty(DefaultPackager.APP_VENDOR_NAME_BUILD_PROP)); map.put("iphoneos:bundle.id", mosyncProject.getProperty("iphone:bundle.id")); response.getWriter().write(plistTemplate.resolve(map)); Util.safeClose(response.getWriter()); } private void generateIndex(HttpServletResponse response) throws IOException { response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); response.setStatus(HttpServletResponse.SC_OK); PrintWriter output = response.getWriter(); Template indexTemplate = new Template(getClass().getResource( "/templates/index.html.template")); StringBuffer projectList = new StringBuffer(); int projectCount = 0; TreeMap<Long, MoSyncProject> sortedByBuildTime = new TreeMap<Long, MoSyncProject>(); for (MoSyncProject project : projects.keySet()) { IBuildVariant variant = this.projects.get(project); IBuildState buildState = project.getBuildState(variant); IBuildResult buildResult = buildState.getBuildResult(); if (buildResult != null) { long buildTimestamp =buildResult.getTimestamp(); sortedByBuildTime.put(-buildTimestamp, project); } } for (MoSyncProject project : sortedByBuildTime.values()) { IBuildVariant variant = this.projects.get(project); IBuildState buildState = project.getBuildState(variant); List<File> ipaFile = buildState.getBuildResult().getBuildResult().get(IBuildResult.MAIN); if (!ipaFile.isEmpty() && ipaFile.get(0).exists()) { projectCount++; String projectName = project.getName(); String url = URLEncoder.encode(getBaseURL(), "UTF-8"); projectList.append("<div class=\"appitem\">"); projectList.append(MessageFormat.format("<div class=\"r\"><img src=\"./{0}/icon57x57.png\"></div>", projectName)); projectList.append("<div class=\"l\">"); projectList.append(project.getName()); projectList.append("</div><br/>"); long buildTimestamp = buildState.getBuildResult().getTimestamp(); boolean fairlyRecent = System.currentTimeMillis() - buildTimestamp < 18 * 3600 * 1000; DateFormat format = fairlyRecent ? DateFormat.getTimeInstance(DateFormat.SHORT) : DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT); String buildTime = format.format(new Date(buildTimestamp)); long fileSize = ipaFile.get(0).length(); projectList.append(MessageFormat.format( "<div class=\"q\"><a href=\"itms-services://?action=download-manifest&url={0}/{1}.plist\">Download App</a>" + "<small><br/>Build time: {2}" + "<br/>App size: {3}" + "<br/>Configuration: {4}</small></div>", url, projectName, buildTime, Util.dataSize(fileSize), variant.getConfigurationId())); projectList.append("<br/>"); projectList.append("</div>"); } } HashMap<String, String> map = new HashMap<String, String>(); map.put("project-list", projectList.toString()); map.put("project-count", Integer.toString(projectCount)); output.write(indexTemplate.resolve(map)); Util.safeClose(output); } private String getBaseURL() throws IOException { InetAddress localHost = InetAddress.getLocalHost(); String host = localHost.getHostAddress(); return new URL("http", host, getPort(), "").toExternalForm(); } }