package com.mobilesorcery.sdk.core.build; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.ui.IMemento; import org.eclipse.ui.XMLMemento; import com.mobilesorcery.sdk.core.Cache; import com.mobilesorcery.sdk.core.CoreMoSyncPlugin; import com.mobilesorcery.sdk.core.IBuildSession; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.Pair; import com.mobilesorcery.sdk.core.Util; import com.mobilesorcery.sdk.core.Version; import com.mobilesorcery.sdk.core.build.IBuildStepFactoryExtension.Position; public class BuildSequence implements IBuildSequence { public final static String BUILD_DESC_FILE_NAME = ".build"; private static final String VERSION_KEY = "version"; private static final Version VERSION_1_0 = new Version("1.0"); private static final Version CURRENT_VERSION = new Version("1.1"); private final MoSyncProject project; private ArrayList<IBuildStepFactory> buildStepFactories = new ArrayList<IBuildStepFactory>(); private ArrayList<IBuildStep> buildSteps; private Version formatVersion = CURRENT_VERSION; private final static String[] BASE_SEQUENCE = new String[] { ResourceBuildStep.ID, NativeLibBuildStep.ID, CompileBuildStep.ID, LinkBuildStep.ID, PackBuildStep.ID, CopyBuildResultBuildStep.ID }; // Can leak max 8 prjs. private static Cache<MoSyncProject, BuildSequence> cache = new Cache<MoSyncProject, BuildSequence> (8); public BuildSequence(MoSyncProject project) { this.project = project; load(); } /** * Returns a cached copy the build sequence for a specific project. * @param project * @return */ public static BuildSequence getCached(MoSyncProject project) { // TODO: cache all projects, dispose when project is disposed. if (project.isDisposed()) { clearCache(project); return null; } BuildSequence cached = cache.get(project); if (cached == null) { cached = new BuildSequence(project); cache.put(project, cached); } return cached; } public static void clearCache(MoSyncProject project) { cache.remove(project); } private void initDefaultFactories() { for (int i = 0; i < BASE_SEQUENCE.length; i++) { addDefaultFactory(i, BASE_SEQUENCE[i]); } for (String factoryId : CoreMoSyncPlugin.getDefault().getBuildStepFactories()) { IBuildStepFactoryExtension ext = CoreMoSyncPlugin.getDefault().getBuildStepFactoryExtension(factoryId); if (ext != null) { Pair<Position, String> pos = ext.getDefaultPosition(); if (pos != null && pos.first != Position.NONE && pos.second != null) { // We only allow positions relative to intrinsic factories. int ix = getIndex(pos.second); addDefaultFactory(ix + (pos.first == Position.BEFORE ? 0 : 1), factoryId); } } } } private int getIndex(String id) { for (int i = 0; i < buildStepFactories.size(); i++) { if (buildStepFactories.get(i).getId().equals(id)) { return i; } } return -1; } private boolean isDefaultSequence() { int baseSequenceIx = 0; for (IBuildStepFactory buildStepFactory : buildStepFactories) { if (!buildStepFactory.isDefault()) { return false; } String id = buildStepFactory.getId(); IBuildStepFactoryExtension ext = CoreMoSyncPlugin.getDefault(). getBuildStepFactoryExtension(id); if (ext == null) { // Then we'd better be at the proper base sequence position! if (!BASE_SEQUENCE[baseSequenceIx].equals(id)) { return false; } baseSequenceIx++; } else { Pair<Position, String> pos = ext.getDefaultPosition(); if (pos == null || pos.first == Position.NONE || pos.second == null) { return false; } boolean correctPositionBefore = pos.first == Position.BEFORE && pos.second.equals(BASE_SEQUENCE[baseSequenceIx]); boolean correctPositionAfter = baseSequenceIx > 0 && pos.first == Position.AFTER && pos.second.equals(BASE_SEQUENCE[baseSequenceIx - 1]); if (!correctPositionAfter && !correctPositionBefore) { return false; } } } return baseSequenceIx == BASE_SEQUENCE.length; } private void addDefaultFactory(int ix, String id) { IBuildStepFactory factory = CoreMoSyncPlugin.getDefault().createBuildStepFactory(id); buildStepFactories.add(ix, factory); } private void load() { FileReader input = null; try { IPath buildFilePath = getBuildFilePath(); if (buildFilePath.toFile().exists()) { buildStepFactories.clear(); input = new FileReader(buildFilePath.toFile()); XMLMemento memento = XMLMemento.createReadRoot(input); String formatVersionStr = memento.getString(VERSION_KEY); formatVersion = formatVersionStr == null ? VERSION_1_0 : new Version(formatVersionStr); IMemento[] buildSteps = memento.getChildren("buildStep"); for (int i = 0; i < buildSteps.length; i++) { IMemento buildStep = buildSteps[i]; String buildStepId = buildStep.getString("type"); IBuildStepFactory factory = CoreMoSyncPlugin.getDefault().createBuildStepFactory(buildStepId); if (factory != null) { factory.load(buildStep); this.buildStepFactories.add(factory); } else { CoreMoSyncPlugin.getDefault().getLog().log(new Status(IStatus.INFO, CoreMoSyncPlugin.PLUGIN_ID, MessageFormat.format("Build step factory {0} missing", buildStepId))); } } upgrade(); } else { initDefaultFactories(); } } catch (Exception e) { CoreMoSyncPlugin.getDefault().log(e); } finally { Util.safeClose(input); } } public void upgrade() throws CoreException, IOException { if (CURRENT_VERSION.isNewer(formatVersion)) { if (getBuildStepFactories(NativeLibBuildStep.Factory.class).isEmpty()) { List<IBuildStepFactory> factories = getBuildStepFactories(); ArrayList<IBuildStepFactory> newFactories = new ArrayList<IBuildStepFactory>(); for (IBuildStepFactory factory : factories) { if (CompileBuildStep.ID.equals(factory.getId())) { newFactories.add(new NativeLibBuildStep.Factory()); } newFactories.add(factory); } try { apply(newFactories); } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, e.getMessage(), e)); } } formatVersion = CURRENT_VERSION; save(); } } public void save() throws IOException { FileWriter output = null; try { File buildFilePath = getBuildFilePath().toFile(); if (isDefaultSequence()) { buildFilePath.delete(); } else { XMLMemento memento = XMLMemento.createWriteRoot("buildSequence"); memento.putString(VERSION_KEY, formatVersion.asCanonicalString()); for (IBuildStepFactory buildStepFactory : buildStepFactories) { IMemento buildStepMemento = memento.createChild("buildStep"); buildStepMemento.putString("type", buildStepFactory.getId()); buildStepFactory.store(buildStepMemento); } output = new FileWriter(buildFilePath); memento.save(output); } } finally { Util.safeClose(output); } } private IPath getBuildFilePath() { return project.getWrappedProject().getLocation().append(BUILD_DESC_FILE_NAME); } @Override public List<IBuildStep> getBuildSteps(IBuildSession session) { if (this.buildSteps == null) { buildSteps = new ArrayList<IBuildStep>(); for (IBuildStepFactory factory : this.buildStepFactories) { IBuildStep buildStep = factory.create(); if (buildStep != null && buildStep.shouldAdd(session)) { buildSteps.add(buildStep); } } } return buildSteps; } public void assertValid(IBuildSession session) throws CoreException { List<IBuildStep> buildSteps = getBuildSteps(session); HashSet<String> buildStepsBefore = new HashSet<String>(); for (IBuildStep buildStep : buildSteps) { String[] dependees = buildStep.getDependees(); if (dependees != null) { for (String dependee : buildStep.getDependees()) { if (!buildStepsBefore.contains(dependee)) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, MessageFormat.format("The build step {0} requires this build step with this id to be performed prior to it: {1}", buildStep.getName(), dependee))); } } } buildStepsBefore.add(buildStep.getId()); } } /** * Returns the set of build step factories. This method * returns a copy of the internal list, so to edit the build * sequence, use the {@link #apply(List)} method. * @return */ public List<IBuildStepFactory> getBuildStepFactories() { return new ArrayList<IBuildStepFactory>(this.buildStepFactories); } /** * Will apply a list of build step factories to this sequence * and save it. * @param buildStepFactories * @throws IOException */ public void apply(List<IBuildStepFactory> buildStepFactories) throws IOException { this.buildSteps = null; this.buildStepFactories = new ArrayList<IBuildStepFactory>(buildStepFactories); save(); } /** * Sets this build sequence to the default sequence. */ public void setToDefault() { buildStepFactories.clear(); initDefaultFactories(); } public <T extends IBuildStepFactory> List<T> getBuildStepFactories(Class<T> clazz) { ArrayList<T> result = new ArrayList<T>(); for (IBuildStepFactory factory : buildStepFactories) { if (factory.getClass().isAssignableFrom(clazz)) { result.add((T) factory); } } return result; } }