/******************************************************************************* * Copyright (c) 2013, 2016 Red Hat, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Red Hat Inc. - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.thym.core.engine; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.osgi.util.NLS; import org.eclipse.thym.core.HybridCore; import org.eclipse.thym.core.HybridProject; import org.eclipse.thym.core.config.Engine; import org.eclipse.thym.core.config.Widget; import org.eclipse.thym.core.config.WidgetModel; import org.eclipse.thym.core.engine.internal.cordova.CordovaEngineProvider; import org.eclipse.thym.core.extensions.PlatformSupport; import org.eclipse.thym.core.internal.cordova.CordovaCLI; import org.eclipse.thym.core.internal.cordova.CordovaCLI.Command; import org.eclipse.thym.core.internal.cordova.ErrorDetectingCLIResult; import org.eclipse.thym.core.platform.PlatformConstants; import org.osgi.framework.Version; import com.google.gson.JsonIOException; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; /** * API for managing the engines for a {@link HybridProject}. * * @author Gorkem Ercan * */ public class HybridMobileEngineManager { private final HybridProject project; public HybridMobileEngineManager(HybridProject project){ this.project = project; } /** * Returns the effective engines for project. * Active engines are determined as follows. * <ol> * <li> * if any platforms are listed in the platforms.json file, these are returned first, without * checking config.xml. * </li> * <li> * if <i>engine</i> entries exist on config.xml match them to installed cordova engines. * </li> * @see HybridMobileEngineManager#defaultEngines() * @return possibly empty array of {@link HybridMobileEngine}s */ public HybridMobileEngine[] getActiveEngines(){ HybridMobileEngine[] platformJsonEngines = getActiveEnginesFromPlatformsJson(); if (platformJsonEngines.length > 0) { return platformJsonEngines; } try{ WidgetModel model = WidgetModel.getModel(project); Widget w = model.getWidgetForRead(); List<Engine> engines = null; if(w != null ){ engines = w.getEngines(); } if(engines != null && !engines.isEmpty() ){ CordovaEngineProvider engineProvider = new CordovaEngineProvider(); ArrayList<HybridMobileEngine> activeEngines = new ArrayList<HybridMobileEngine>(); final List<HybridMobileEngine> availableEngines = engineProvider.getAvailableEngines(); for (Engine engine : engines) { for (HybridMobileEngine hybridMobileEngine : availableEngines) { if(engineMatches(engine, hybridMobileEngine)){ activeEngines.add(hybridMobileEngine); break; } } } return activeEngines.toArray(new HybridMobileEngine[activeEngines.size()]); } } catch (CoreException e) { HybridCore.log(IStatus.WARNING, "Engine information can not be read", e); } return new HybridMobileEngine[0]; } private boolean engineMatches(Engine configEngine, HybridMobileEngine engine){ //null checks needed: sometimes we encounter engines without a name or version attribute. if(engine.isManaged()){ // Since cordova uses semver, version numbers in config.xml can begin with '~' or '^'. if (configEngine.getSpec() != null) { String spec = configEngine.getSpec(); if (spec.startsWith("~") || spec.startsWith("^")) { spec = spec.substring(1); } return configEngine.getName() != null && configEngine.getName().equals(engine.getId()) && spec.equals(engine.getVersion()); } else { return false; } }else{ return engine.getLocation().isValidPath(configEngine.getSpec()) && engine.getLocation().equals(new Path(configEngine.getSpec())); } } /** * Returns the active engines for the project by looking at * the values stored in platforms.json. * * </p> * If no engines are found in platforms.json, returns an empty array. * The file platforms.json is where the currently active cordova engines * are stored, (semi-)independently of what is stored in config.xml. * * @see HybridMobileEngineManager#defaultEngines() * @see HybridMobileEngineManager#getActiveEngines() * @return possibly empty array of {@link HybridMobileEngine}s */ public HybridMobileEngine[] getActiveEnginesFromPlatformsJson(){ try { IFile file = project.getProject().getFile(PlatformConstants.PLATFORMS_JSON_PATH); if (!file.exists()) { return new HybridMobileEngine[0]; } List<HybridMobileEngine> activeEngines = new ArrayList<HybridMobileEngine>(); JsonParser parser = new JsonParser(); JsonObject root = parser.parse(new InputStreamReader(file.getContents())).getAsJsonObject(); for (PlatformSupport support : HybridCore.getPlatformSupports()) { String platform = support.getPlatformId(); if (root.has(platform)) { HybridMobileEngine engine = getHybridMobileEngine(platform, root.get(platform).getAsString()); if (engine != null) { activeEngines.add(engine); } } } return activeEngines.toArray(new HybridMobileEngine[activeEngines.size()]); } catch (JsonIOException e) { HybridCore.log(IStatus.WARNING, "Error reading input stream from platforms.json", e); } catch (JsonSyntaxException e) { HybridCore.log(IStatus.WARNING, "platforms.json has errors", e); } catch (CoreException e) { HybridCore.log(IStatus.WARNING, "Error while opening platforms.json", e); } return new HybridMobileEngine[0]; } /** * Returns the HybridMobileEngine that corresponds to the provide name and spec. * Searches through available engines for a match, and may return null if no * matching engine is found. * * @return The HybridMobileEngine corresponding to name and spec, or null if * a match cannot be found. */ private HybridMobileEngine getHybridMobileEngine(String name, String spec) { CordovaEngineProvider engineProvider = new CordovaEngineProvider(); final List<HybridMobileEngine> availableEngines = engineProvider.getAvailableEngines(); for (HybridMobileEngine engine : availableEngines) { if (engine.isManaged()) { if (engine.getId().equals(name) && engine.getVersion().equals(spec)) { return engine; } } else { if (engine.getId().equals(name) && engine.getLocation().toString().equals(spec)) { return engine; } } } return null; } /** * Returns the {@link HybridMobileEngine}s specified within Thym preferences. * * </p> * If no engines have been added, returns an empty array. Otherwise returns * either the user's preference, or, by default, the most recent version * available for each platform. * * @see HybridMobileEngineManager#getActiveEngines() * @return possibly empty array of {@link HybridMobileEngine}s */ public static HybridMobileEngine[] defaultEngines() { CordovaEngineProvider engineProvider = new CordovaEngineProvider(); List<HybridMobileEngine> availableEngines = engineProvider.getAvailableEngines(); if(availableEngines == null || availableEngines.isEmpty() ){ return new HybridMobileEngine[0]; } ArrayList<HybridMobileEngine> defaults = new ArrayList<HybridMobileEngine>(); String pref = Platform.getPreferencesService().getString(PlatformConstants.HYBRID_UI_PLUGIN_ID, PlatformConstants.PREF_DEFAULT_ENGINE, null, null); if(pref != null && !pref.isEmpty()){ String[] engineStrings = pref.split(","); for (String engineString : engineStrings) { String[] engineInfo = engineString.split(":"); for (HybridMobileEngine hybridMobileEngine : availableEngines) { if (hybridMobileEngine.isManaged()) { if (engineInfo[0].equals(hybridMobileEngine.getId()) && engineInfo[1].equals(hybridMobileEngine.getVersion())) { defaults.add(hybridMobileEngine); } } else { if (engineInfo[0].equals(hybridMobileEngine.getId()) && engineInfo[1].equals(hybridMobileEngine.getLocation().toString())) { defaults.add(hybridMobileEngine); } } } } }else{ HashMap<String, HybridMobileEngine> platforms = new HashMap<String, HybridMobileEngine>(); for (HybridMobileEngine hybridMobileEngine : availableEngines) { if(platforms.containsKey(hybridMobileEngine.getId())){ HybridMobileEngine existing = platforms.get(hybridMobileEngine.getId()); try{ Version ev = Version.parseVersion(existing.getVersion()); Version hv = Version.parseVersion(hybridMobileEngine.getVersion()); if(hv.compareTo(ev) >0 ){ platforms.put(hybridMobileEngine.getId(), hybridMobileEngine); } }catch(IllegalArgumentException e){ //catch the version parse errors because version field may actually contain //git urls and local paths. } }else{ platforms.put(hybridMobileEngine.getId(),hybridMobileEngine); } } defaults.addAll(platforms.values()); } return defaults.toArray(new HybridMobileEngine[defaults.size()]); } /** * Persists engine information to config.xml. * Removes existing engines form the project. * Calls cordova prepare so that the new engines are restored. * * @param engine * @throws CoreException */ public void updateEngines(final HybridMobileEngine[] engines) throws CoreException{ WorkspaceJob updateJob = new WorkspaceJob(NLS.bind("Update Cordova Engines for {0}",project.getProject().getName()) ) { @Override public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { WidgetModel model = WidgetModel.getModel(project); Widget w = model.getWidgetForEdit(); List<Engine> existingEngines = w.getEngines(); CordovaCLI cordova = CordovaCLI.newCLIforProject(project); SubMonitor sm = SubMonitor.convert(monitor,100); if(existingEngines != null ){ for (Engine existingEngine : existingEngines) { if(isEngineRemoved(existingEngine, engines)){ cordova.platform(Command.REMOVE, sm,existingEngine.getName()); } w.removeEngine(existingEngine); } } sm.worked(30); for (HybridMobileEngine engine : engines) { Engine e = model.createEngine(w); e.setName(engine.getId()); if(!engine.isManaged()){ e.setSpec(engine.getLocation().toString()); }else{ e.setSpec(engine.getVersion()); } w.addEngine(e); } model.save(); IStatus status = Status.OK_STATUS; if(w.getEngines() != null && !w.getEngines().isEmpty()){ status = cordova.prepare(sm.newChild(40), "").convertTo(ErrorDetectingCLIResult.class).asStatus(); } project.getProject().refreshLocal(IResource.DEPTH_INFINITE, sm.newChild(30)); sm.done(); return status; } }; ISchedulingRule rule = ResourcesPlugin.getWorkspace().getRuleFactory().modifyRule(this.project.getProject()); updateJob.setRule(rule); updateJob.schedule(); } /** * Updates active Cordova engines based on what is written to * config.xml by calling cordova update or cordova add, depending * on context. */ public void resyncWithConfigXml() { WorkspaceJob prepareJob = new WorkspaceJob(NLS.bind("Updating project from config.xml for {0}", project.getProject().getName())) { @Override public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { if (project == null) { String err = "Updating from config.xml: Could not get HybridProject"; HybridCore.log(IStatus.WARNING, err, null); return new Status(IStatus.WARNING, HybridCore.PLUGIN_ID, err); } HybridMobileEngine[] activeEngines = project.getActiveEnginesFromPlatformsJson(); CordovaCLI cordova = CordovaCLI.newCLIforProject(project); MultiStatus status = new MultiStatus(HybridCore.PLUGIN_ID, 0, "Errors updating engines from config.xml", null); IStatus subStatus = Status.OK_STATUS; SubMonitor sm = SubMonitor.convert(monitor, 100); Widget widget = WidgetModel.getModel(project).getWidgetForEdit(); if (widget != null) { List<Engine> configEngines = widget.getEngines(); if (configEngines == null) { if(activeEngines == null){ return status; } SubMonitor loopMonitor = sm.newChild(70).setWorkRemaining(activeEngines.length); for(HybridMobileEngine engine: activeEngines){ subStatus = cordova.platform(Command.REMOVE, loopMonitor.newChild(1), engine.getId()) .convertTo(ErrorDetectingCLIResult.class).asStatus(); status.add(subStatus); } } else { SubMonitor loopMonitor = sm.newChild(70).setWorkRemaining(configEngines.size()); for (Engine e : configEngines) { String platformSpec = e.getName() + "@" + e.getSpec(); if (checkPlatformInstalled(activeEngines, e.getName())) { subStatus = cordova.platform(Command.UPDATE, loopMonitor.newChild(1), platformSpec) .convertTo(ErrorDetectingCLIResult.class).asStatus(); } else { subStatus = cordova.platform(Command.ADD, loopMonitor.newChild(1), platformSpec) .convertTo(ErrorDetectingCLIResult.class).asStatus(); } status.add(subStatus); } } } project.getProject().refreshLocal(IResource.DEPTH_INFINITE, sm.newChild(30)); return status; } }; ISchedulingRule rule = ResourcesPlugin.getWorkspace().getRuleFactory() .modifyRule(project.getProject()); prepareJob.setRule(rule); prepareJob.schedule(); } private boolean checkPlatformInstalled(HybridMobileEngine[] activeEngines, String engineName) { for (HybridMobileEngine engine : activeEngines) { if (engine.getId().equals(engineName)) { return true; } } return false; } private boolean isEngineRemoved(final Engine engine, final HybridMobileEngine[] engines){ for (HybridMobileEngine hybridMobileEngine : engines) { if(hybridMobileEngine.getId().equals(engine.getName()) && hybridMobileEngine.getVersion().equals(engine.getSpec())){ return false; } } return true; } }