/* Copyright (C) 2009 Mobile Sorcery AB This program is free software; you can redistribute it and/or modify it under the terms of the Eclipse Public License v1.0. 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 Eclipse Public License v1.0 for more details. You should have received a copy of the Eclipse Public License v1.0 along with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html */ package com.mobilesorcery.sdk.builder.java; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.jar.Attributes; import java.util.jar.Manifest; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import com.mobilesorcery.sdk.core.AbstractPackager; import com.mobilesorcery.sdk.core.CommandLineBuilder; import com.mobilesorcery.sdk.core.DefaultPackager; import com.mobilesorcery.sdk.core.IBuildResult; import com.mobilesorcery.sdk.core.IBuildSession; import com.mobilesorcery.sdk.core.IBuildVariant; import com.mobilesorcery.sdk.core.IFileTreeDiff; import com.mobilesorcery.sdk.core.MoSyncBuilder; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.MoSyncTool; import com.mobilesorcery.sdk.core.ParameterResolver; import com.mobilesorcery.sdk.core.ParameterResolverException; import com.mobilesorcery.sdk.core.SecurePropertyException; import com.mobilesorcery.sdk.core.Util; import com.mobilesorcery.sdk.core.Version; import com.mobilesorcery.sdk.core.security.IApplicationPermissions; import com.mobilesorcery.sdk.internal.builder.MoSyncIconBuilderVisitor; import com.mobilesorcery.sdk.profiles.IProfile; import com.mobilesorcery.sdk.ui.DefaultMessageProvider; // REMOVE THIS ONCE EVERYTHING ELSE IS IN PLACE! -- FOR NOW, THE BLACKBERRY PACKAGER USES IT! public class JavaPackager2 extends AbstractPackager { private final String m_zipLoc; private final String m_iconInjectorLoc; public JavaPackager2() { MoSyncTool tool = MoSyncTool.getDefault(); m_zipLoc = tool.getBinary("zip").toOSString(); m_iconInjectorLoc = tool.getBinary("icon-injector").toOSString(); } @Override public void createPackage(MoSyncProject project, IBuildSession session, IBuildVariant variant, IFileTreeDiff diff, IBuildResult buildResult) throws CoreException { createPackage(project, variant, buildResult, true); } public void createPackage(MoSyncProject project, IBuildVariant variant, IBuildResult buildResult, boolean doSign) throws CoreException { DefaultPackager internal = new DefaultPackager(project, variant); IProfile targetProfile = variant.getProfile(); File runtimeDir = internal.resolveFile("%runtime-dir%"); File compileOut = internal.resolveFile("%compile-output-dir%"); internal.setParameter("D", shouldUseDebugRuntimes(project, variant) ? "D" : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ try { File packageOutputDir = internal.resolveFile("%package-output-dir%"); //$NON-NLS-1$ packageOutputDir.mkdirs(); Version appVersion = new Version(internal.getParameters().get(DefaultPackager.APP_VERSION)); IApplicationPermissions permissions = project.getPermissions(); File projectJar = internal.resolveFile("%package-output-dir%/%app-name%.jar"); //$NON-NLS-1$ File projectJad = internal.resolveFile("%package-output-dir%/%app-name%.jad"); //$NON-NLS-1$ projectJar.delete(); projectJad.delete(); String appVendorName = internal.getParameters().get(DefaultPackager.APP_VENDOR_NAME); String appName = internal.getParameters().get(DefaultPackager.APP_NAME); File manifest = internal.resolveFile("%compile-output-dir%/META-INF/MANIFEST.MF"); //$NON-NLS-1$ createManifest(variant, appName, appVendorName, permissions, appVersion, manifest); // Need to set execution dir, o/w zip will not understand what we // really want. internal.getExecutor().setExecutionDirectory(manifest.getParentFile().getParent()); { String runtime = internal.resolve("MoSyncRuntime%D%.jar"); Util.copyFile(new NullProgressMonitor(), new File(runtimeDir, runtime), projectJar); } Util.copyFile(new NullProgressMonitor(), new File(runtimeDir, "config.h"), new File(packageOutputDir, "config.h")); internal.runCommandLine(m_zipLoc, "-j", "-9", projectJar.getAbsolutePath(), new File(compileOut, "program").getAbsolutePath()); internal.runCommandLine(m_zipLoc, "-r", "-9", projectJar.getAbsolutePath(), "META-INF"); File resources = new File(compileOut, "resources"); //$NON-NLS-1$ if (resources.exists()) { internal.runCommandLine(m_zipLoc, "-j", "-9", projectJar.getAbsolutePath(), resources.getAbsolutePath()); } createJAD(variant, appName, appVendorName, permissions, appVersion, projectJad, projectJar); MoSyncIconBuilderVisitor visitor = new MoSyncIconBuilderVisitor(); visitor.setProject(project.getWrappedProject()); IResource[] iconFiles = visitor.getIconFiles(); if (iconFiles.length > 0) { Object xObj = targetProfile.getProperties().get("MA_PROF_CONST_ICONSIZE_X"); //$NON-NLS-1$ Object yObj = targetProfile.getProperties().get("MA_PROF_CONST_ICONSIZE_Y"); //$NON-NLS-1$ if (xObj != null && yObj != null) { String sizeStr = xObj + "x" + yObj; //$NON-NLS-1$ internal.runCommandLine(m_iconInjectorLoc, "-src", iconFiles[0].getLocation().toOSString(), "-size", sizeStr, "-platform", "j2me", "-dst", projectJar.getAbsolutePath()); } else { internal.getConsole().addMessage("Unable to build icon; profile has no icon information (MA_PROF_CONST_ICONSIZE_X, MA_PROF_CONST_ICONSIZE_Y"); } } signPackage(internal, project, variant, projectJad, projectJar); buildResult.setBuildResult(IBuildResult.MAIN, projectJar); buildResult.setBuildResult(Activator.JAD, projectJad); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, "com.mobilesorcery.sdk.builder.java", Messages.JavaPackager_PackageError, e)); //$NON-NLS-1$ } } private void signPackage(DefaultPackager internal, MoSyncProject project, IBuildVariant variant, File projectJad, File projectJar) throws IOException, SecurePropertyException, CoreException, ParameterResolverException { List<KeystoreCertificateInfo> keystoreCertInfos = KeystoreCertificateInfo.load( PropertyInitializer.JAVAME_KEYSTORE_CERT_INFOS, project, project.getSecurePropertyOwner()); ParameterResolver resolver = MoSyncBuilder.createParameterResolver(project, variant); for (KeystoreCertificateInfo keystoreCertInfo : keystoreCertInfos) { String keystore = Util.replace(keystoreCertInfo.getKeystoreLocation(), resolver); String alias = keystoreCertInfo.getAlias(); String storepass = keystoreCertInfo.getKeystorePassword(); String keypass = keystoreCertInfo.getKeyPassword(); if (!DefaultMessageProvider.isEmpty(keystoreCertInfo.validate(false, resolver))) { throw new CoreException(new Status(IStatus.OK, Activator.PLUGIN_ID, "No or invalid key/keystore password for java signing. Please note that for security reasons, passwords are locally stored. You may need to set the password in the Java preference page.")); } MoSyncTool moSyncTool = MoSyncTool.getDefault(); String javaPath = moSyncTool.getJava().toOSString(); CommandLineBuilder addCertCmd = new CommandLineBuilder(javaPath); String jadToolPath = moSyncTool.getMoSyncBin().append("javame/JadTool.jar").toOSString(); addCertCmd.flag("-jar").with(jadToolPath); addCertCmd.flag("-addcert"); addCertCmd.flag("-alias").with(alias); addCertCmd.flag("-keystore").with(keystore); addCertCmd.flag("-inputjad").with(projectJad); addCertCmd.flag("-outputjad").with(projectJad); addCertCmd.flag("-storepass", true).with(storepass); assertOk(internal.runCommandLine(addCertCmd.asArray(), addCertCmd.toHiddenString())); CommandLineBuilder addJarSigCmd = new CommandLineBuilder(javaPath); addJarSigCmd.flag("-jar").with(jadToolPath); addJarSigCmd.flag("-addjarsig"); addJarSigCmd.flag("-jarfile").with(projectJar); addJarSigCmd.flag("-keystore").with(keystore); addJarSigCmd.flag("-storepass", true).with(storepass); addJarSigCmd.flag("-alias").with(alias); addJarSigCmd.flag("-keypass", true).with(keypass); addJarSigCmd.flag("-inputjad").with(projectJad); addJarSigCmd.flag("-outputjad").with(projectJad); assertOk(internal.runCommandLine(addJarSigCmd.asArray(), addJarSigCmd.toHiddenString())); } } private void assertOk(int errorCode) { if (errorCode != 0) { throw new IllegalArgumentException("Tool execution failed"); } } private void createManifest(IBuildVariant variant, String appName, String vendorName, IApplicationPermissions permissions, Version version, File manifestFile) throws IOException { manifestFile.getParentFile().mkdirs(); Manifest manifest = getManifest(variant, null, appName, vendorName, permissions, version, false); FileOutputStream manifestOutput = new FileOutputStream(manifestFile); try { manifest.write(manifestOutput); } finally { Util.safeClose(manifestOutput); } } private void createJAD(IBuildVariant variant, String projectName, String vendorName, IApplicationPermissions permissions, Version version, File jadFile, File jar) throws IOException { Manifest jad = getManifest(variant, jar, projectName, vendorName, permissions, version, true); Set<Entry<Object, Object>> entries = jad.getMainAttributes().entrySet(); FileWriter jadOutput = new FileWriter(jadFile); try { for (Entry entry : entries) { // JAD format: key COLON SPACE value NEWLINE String key = entry.getKey().toString(); String value = entry.getValue().toString(); jadOutput.write(key + ": " + value + "\n"); } } finally { Util.safeClose(jadOutput); } } private Manifest getManifest(IBuildVariant variant, File jar, String appName, String vendorName, IApplicationPermissions permissions, Version version, boolean isJad) throws IOException { IProfile profile = variant.getProfile(); boolean isCLDC_10 = profile != null && Boolean.TRUE.equals(profile.getProperties().get("MA_PROF_SUPPORT_CLDC_10")); Manifest result = new Manifest(); Attributes mainAttr = result.getMainAttributes(); if (!isJad) { mainAttr.putValue("Manifest-Version", "1.0"); } mainAttr.putValue("MIDlet-Vendor", vendorName); mainAttr.putValue("MIDlet-Name", appName); mainAttr.putValue("MIDlet-1", appName + ", " + appName + ".png" + ", MAMidlet"); String[] midletPermissions = getMIDletPermissions(permissions, false); String[] midletOptPermissions = getMIDletPermissions(permissions, true); if (midletPermissions.length > 0) { mainAttr.putValue("MIDlet-Permissions", Util.join(midletPermissions, ", ")); } if (midletOptPermissions.length > 0) { mainAttr.putValue("MIDlet-Permissions-Opt", Util.join(midletOptPermissions, ", ")); } mainAttr.putValue("MIDlet-Version", version.asCanonicalString(Version.MICRO)); mainAttr.putValue("MicroEdition-Configuration", isCLDC_10 ? "CLDC-1.0" : "CLDC-1.1"); mainAttr.putValue("MicroEdition-Profile", "MIDP-2.0"); if (isJad) { long jarSize = jar.length(); String jarName = jar.getName(); mainAttr.putValue("MIDlet-Jar-Size", Long.toString(jarSize)); mainAttr.putValue("MIDlet-Jar-URL", jarName); } return result; } private String[] getMIDletPermissions(IApplicationPermissions permissions, boolean isOptional) { ArrayList<String> result = new ArrayList<String>(); TreeSet<String> reqPermissions = new TreeSet<String>(); TreeSet<String> optPermissions = new TreeSet<String>(); MIDletPermissions.toMIDletPermissions(permissions, reqPermissions, optPermissions); result.addAll(isOptional ? optPermissions : reqPermissions); return result.toArray(new String[result.size()]); } }