/* 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.core;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
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.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.core.resources.FileInfoMatcherDescription;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceFilterDescription;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.XMLMemento;
import com.mobilesorcery.sdk.core.security.IApplicationPermissions;
import com.mobilesorcery.sdk.internal.BuildState;
import com.mobilesorcery.sdk.internal.SecureProperties;
import com.mobilesorcery.sdk.internal.convert.MoSyncProjectConverter1_2;
import com.mobilesorcery.sdk.internal.convert.MoSyncProjectConverter1_4;
import com.mobilesorcery.sdk.internal.convert.MoSyncProjectConverter1_7;
import com.mobilesorcery.sdk.internal.dependencies.LibraryLookup;
import com.mobilesorcery.sdk.internal.security.ApplicationPermissions;
import com.mobilesorcery.sdk.profiles.ICompositeDeviceFilter;
import com.mobilesorcery.sdk.profiles.IDeviceFilter;
import com.mobilesorcery.sdk.profiles.IProfile;
import com.mobilesorcery.sdk.profiles.ITargetProfileProvider;
import com.mobilesorcery.sdk.profiles.IVendor;
import com.mobilesorcery.sdk.profiles.ProfileDBManager;
import com.mobilesorcery.sdk.profiles.filter.AbstractDeviceFilter;
import com.mobilesorcery.sdk.profiles.filter.CompositeDeviceFilter;
import com.mobilesorcery.sdk.profiles.filter.DeviceCapabilitiesFilter;
/**
* This is a wrapper to provider mosync-specific capabilities to a vanilla
* IProject, eg special properties, persistence, etc
*
* @author Mattias
*
*/
public class MoSyncProject extends PropertyOwnerBase implements
ITargetProfileProvider {
/**
* An interface for converting older {@link MoSyncProject}s into a newer
* format.
*
* @author Mattias Bybro
*
*/
public interface IConverter {
/**
* Converts an older project into a newer format.
*
* @param project
* @throws CoreException
*/
public void convert(MoSyncProject project) throws CoreException;
}
public final static Comparator<MoSyncProject> NAME_COMPARATOR = new Comparator<MoSyncProject>() {
@Override
public int compare(MoSyncProject p1, MoSyncProject p2) {
return p1.getName().compareTo(p2.getName());
}
};
private final class DeviceFilterListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent event) {
updateProjectSpec();
// Then just pass it along.
firePropertyChange(event);
}
}
/**
* The property change event type that is triggered if the target profile of
* this project changes
*/
public static final String TARGET_PROFILE_CHANGED = "target.profile";
/**
* The property change event type that is triggered if the build
* configuration of this project changes
*/
public static final String BUILD_CONFIGURATION_CHANGED = "build.config";
/**
* The property change event type that is triggered if the build
* configuration support of this project changes
*/
public static final String BUILD_CONFIGURATION_SUPPORT_CHANGED = "build.config.support";
/**
* The file exclude filter property for this project.
*
* @see PathExclusionFilter
*/
public static final String EXCLUDE_FILTER_KEY = "excludes";
/**
* The standard file excludes for this project (usually NOT an a
* per-configuration basis).
*
* @deprecated Move this to the testing plugin
*/
@Deprecated
public static final String STANDARD_EXCLUDES_FILTER_KEY = "standard.excludes";
/**
* The key for the profile manager type associated with this project.
*/
public static final String PROFILE_MANAGER_TYPE_KEY = "profile.mgr.type";
/**
* The project type of MoSync Projects (used only by CDT).
*/
public static final String C_PROJECT_ID = CoreMoSyncPlugin.PLUGIN_ID
+ ".project";
/**
* The property initializer context of MoSyncProjects
*
* @see IPropertyInitializer
*/
public static final String CONTEXT = "com.mobilesorcery.sdk.mosync.project.context";
/**
* The property key for the originating template id of this project.
*/
public static final String TEMPLATE_ID = "template.id";
/**
* The property key for the dependency strategy of this project.
*/
public static final String DEPENDENCY_STRATEGY = "dependency.strategy";
/**
* A constant indicating that this project should use the GCC dependency
* strategy (GCC -MF option).
*/
public static final int GCC_DEPENDENCY_STRATEGY = 0;
/**
* A constant indicating that this project should not use any dependency
* strategy at all (always rebuild).
*/
public static final int NULL_DEPENDENCY_STRATEGY = 1;
/**
* The extension of the XML files that describes the icons used in a
* project.
*/
public static final String ICON_FILE_EXTENSION = ".icon";
private static final String PROJECT = "project";
private static final String TARGET = "target-profile";
private static final String VENDOR_KEY = "vendor";
private static final String PROFILE_KEY = "device";
private static final String PROPERTIES = "properties";
private static final String PROPERTY = "property";
private static final String KEY_KEY = "key";
private static final String VALUE_KEY = "value";
private static final String BUILD_CONFIG = "build.cfg";
private static final String BUILD_CONFIG_TYPES = "types";
private static final String ACTIVE_BUILD_CONFIG = "active.build.cfg";
private static final String BUILD_CONFIG_ID_KEY = "id";
private static final String BUILD_CONFIGS_SUPPORTED = "supports-build-configs";
private static final String ACTIVE_BUILD_CONFIG_KEY = "active";
private static final String VERSION_KEY = "version";
/**
* The format version currently used to store projects.
*
* @see MoSyncProject#getFormatVersion()
*/
public static final Version CURRENT_VERSION = new Version("1.7");
private static final Version VERSION_1_0 = new Version("1");
/**
* The name of the file where this project's <b>shared</b> meta data is
* located.
*/
public static final String MOSYNC_PROJECT_META_DATA_FILENAME = ".mosyncproject";
/**
* The name of the file where this project's <b>local</b> meta data is
* located.
*/
public static final String MOSYNC_LOCAL_PROJECT_META_DATA_FILENAME = ".mosyncproject.local";
private static final int SHARED_PROPERTY = 0;
static final int LOCAL_PROPERTY = 1;
static final int WORKSPACE_LOCAL_PROPERTY = 2;
private static IdentityHashMap<IProject, MoSyncProject> projects = new IdentityHashMap<IProject, MoSyncProject>();
private final IProject project;
private IProfile target;
private static PropertyChangeSupport globalListeners = new PropertyChangeSupport(
new Object());
private final PropertyChangeSupport listeners = new PropertyChangeSupport(
this);
private ICompositeDeviceFilter deviceFilter = new CompositeDeviceFilter(
new IDeviceFilter[0]);
/**
* This is the map holding the shared properties of this project, stored in
* .mosyncproject
*/
private final Map<String, String> sharedProperties = new HashMap<String, String>();
/**
* This is the map holding the user's local properties of this project,
* stored in .mosyncproject.local
*/
private final Map<String, String> localProperties = new HashMap<String, String>();
private final Map<String, String> workspaceLocalProperties = new HashMap<String, String>();
private final CascadingProperties properties = new CascadingProperties(
new Map[] { sharedProperties, localProperties,
workspaceLocalProperties });
private final DeviceFilterListener deviceFilterListener;
private IBuildConfiguration currentBuildConfig;
private final TreeMap<String, IBuildConfiguration> configurations = new TreeMap<String, IBuildConfiguration>(
String.CASE_INSENSITIVE_ORDER);
private boolean isBuildConfigurationsSupported;
private final HashMap<String, SLD> slds = new HashMap<String, SLD>();
private boolean disposed = false;
private final HashMap<IBuildVariant, IBuildState> cachedBuildStates = new HashMap<IBuildVariant, IBuildState>();
private final HashMap<IPropertyOwner, PathExclusionFilter> excludes = new HashMap<IPropertyOwner, PathExclusionFilter>();
private final ApplicationPermissions permissions;
private Version formatVersion = CURRENT_VERSION;
private final ISecurePropertyOwner securePropertyOwner;
private MoSyncProject(IProject project) {
Assert.isNotNull(project);
this.project = project;
this.deviceFilterListener = new DeviceFilterListener();
reinit(false);
permissions = new ApplicationPermissions(this);
addDeviceFilterListener();
securePropertyOwner = new SecureProperties(this, CoreMoSyncPlugin
.getDefault().getPasswordProvider(),
SecureProperties.DEFAULT_SECURE_PROPERTY_SUFFIX);
}
private void addDeviceFilterListener() {
deviceFilter.addPropertyChangeListener(deviceFilterListener);
}
private void removeDeviceFilterListener() {
deviceFilter.removePropertyChangeListener(deviceFilterListener);
}
/**
* Reinitializes this project with the settings file(s). This
* enables external programs to modify the setting file(s) and
* this project to synchronize with the files.
*/
public void reinit(boolean fireEvents) {
Map<String, String> oldProperties = getProperties();
initFromProjectMetaData(null, SHARED_PROPERTY);
initFromProjectMetaData(null, LOCAL_PROPERTY);
initFromProjectMetaData(null, WORKSPACE_LOCAL_PROPERTY);
if (fireEvents) {
Map<String, String> newProperties = getProperties();
Set<String> changedProperties = BuildState.getPropertiesDiff(oldProperties, newProperties);
for (String changedProperty : changedProperties) {
Object oldValue = oldProperties.get(changedProperty);
Object newValue = newProperties.get(changedProperty);
firePropertyChange(new PropertyChangeEvent(this, changedProperty, oldValue, newValue));
}
if (!changedProperties.isEmpty()) {
invalidatePropertyDependentObjects();
setProfileManagerType(getProfileManagerType(), true);
}
}
}
private void initFromProjectMetaData(IPath projectMetaDataPath, int store) {
if (projectMetaDataPath == null) {
projectMetaDataPath = getMoSyncProjectMetaDataLocation(store);
}
if (!projectMetaDataPath.toFile().exists()) {
return;
}
FileReader input = null;
try {
// Older projects only have a shared config, so initialize
// everything
// from that first
input = new FileReader(projectMetaDataPath.toFile());
XMLMemento memento = XMLMemento.createReadRoot(input);
String formatVersionStr = memento.getString(VERSION_KEY);
formatVersion = formatVersionStr == null ? VERSION_1_0
: new Version(formatVersionStr);
// Special case; device filters are always shared.
if (store == SHARED_PROPERTY) {
deviceFilter = CompositeDeviceFilter.read(memento);
initAvailableBuildConfigurations(memento);
}
initActiveBuildConfiguration(memento);
initProperties(initPropertiesFromProjectMetaData(memento), store);
initTargetProfileFromProjectMetaData(memento);
} catch (Exception e) {
CoreMoSyncPlugin.getDefault().log(e);
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
CoreMoSyncPlugin.getDefault().log(e);
}
}
}
}
private void initProperties(Map<String, String> properties, int store) {
Map<String, String> oldProperties = getProperties(store);
oldProperties.clear();
oldProperties.putAll(properties);
invalidatePropertyDependentObjects();
}
// Makes sure that objects that are directly dependent
// on properties are invalidated and re-initialized
private void invalidatePropertyDependentObjects() {
if (permissions != null) {
permissions.refresh();
}
}
private void initActiveBuildConfiguration(XMLMemento memento) {
IMemento activeCfg = memento.getChild(ACTIVE_BUILD_CONFIG);
if (activeCfg != null) {
String activeCfgId = activeCfg.getString(ACTIVE_BUILD_CONFIG_KEY);
currentBuildConfig = getBuildConfiguration(activeCfgId);
}
}
private void initAvailableBuildConfigurations(XMLMemento memento) {
Boolean supportsBuildConfigurations = memento
.getBoolean(BUILD_CONFIGS_SUPPORTED);
this.isBuildConfigurationsSupported = supportsBuildConfigurations == null ? false
: supportsBuildConfigurations;
configurations.clear();
IMemento[] cfgs = memento.getChildren(BUILD_CONFIG);
for (int i = 0; i < cfgs.length; i++) {
String id = cfgs[i].getString(BUILD_CONFIG_ID_KEY);
String[] cfgTypes = PropertyUtil.toStrings(cfgs[i]
.getString(BUILD_CONFIG_TYPES));
BuildConfiguration cfg = new BuildConfiguration(this, id, cfgTypes);
// For older versions of the mosync project, the active
// property might be set as an attribute here instead of
// as a separate tag.
if (Boolean.TRUE
.equals(cfgs[i].getBoolean(ACTIVE_BUILD_CONFIG_KEY))) {
currentBuildConfig = cfg;
}
configurations.put(id, cfg);
}
}
private Map<String, String> initPropertiesFromProjectMetaData(
XMLMemento memento) {
Map<String, String> result = new HashMap<String, String>();
IMemento properties = memento.getChild(PROPERTIES);
if (properties != null) {
IMemento[] propertyChildren = properties.getChildren(PROPERTY);
for (int i = 0; propertyChildren != null
&& i < propertyChildren.length; i++) {
String key = propertyChildren[i].getString(KEY_KEY);
String value = propertyChildren[i].getString(VALUE_KEY);
if (key != null && value != null) {
result.put(key, value);
}
}
}
return result;
}
private void initTargetProfileFromProjectMetaData(IMemento memento) {
IMemento targetMemento = memento.getChild(TARGET);
if (targetMemento != null) {
String vendorName = targetMemento.getString(VENDOR_KEY);
String profileName = targetMemento.getString(PROFILE_KEY);
IVendor vendor = vendorName == null ? null : getProfileManager().getVendor(vendorName);
if (vendor != null) {
IProfile targetProfile = vendor.getProfile(profileName);
if (targetProfile != null) {
target = targetProfile;
}
}
}
}
public void updateProjectSpec() {
updateProjectSpec(SHARED_PROPERTY);
updateProjectSpec(LOCAL_PROPERTY);
updateProjectSpec(WORKSPACE_LOCAL_PROPERTY);
}
protected void updateProjectSpec(int store) {
if (!requiresUpdate(store)) {
return;
}
IPath projectMetaDataPath = getMoSyncProjectMetaDataLocation(store);
XMLMemento root = XMLMemento.createWriteRoot(PROJECT);
FileWriter output = null;
try {
output = new FileWriter(projectMetaDataPath.toFile());
// Some properties are meant to be stored only in
// shared properties, others only in local.
if (store == SHARED_PROPERTY) {
saveAvailableBuildConfigurations(root);
deviceFilter.saveState(root);
}
if (store == LOCAL_PROPERTY) {
saveActiveBuildConfiguration(root);
IMemento target = root.createChild(TARGET);
saveTargetProfile(target);
}
IMemento propertiesMemento = root.createChild(PROPERTIES);
saveProperties(propertiesMemento, getProperties(store));
if (store == SHARED_PROPERTY) {
root.putString(VERSION_KEY, formatVersion.asCanonicalString());
}
root.save(output);
output.close();
} catch (IOException e) {
CoreMoSyncPlugin.getDefault().log(e);
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
CoreMoSyncPlugin.getDefault().log(e);
}
}
}
}
private boolean requiresUpdate(int store) {
IPath projectMetaDataPath = getMoSyncProjectMetaDataLocation(store);
// Special case: the local properties; if it exists, we always write it
// anew regardless.
return store != WORKSPACE_LOCAL_PROPERTY
|| projectMetaDataPath.toFile().exists()
|| !getProperties(store).isEmpty();
}
private void saveActiveBuildConfiguration(XMLMemento root) {
if (currentBuildConfig != null) {
IMemento node = root.createChild(ACTIVE_BUILD_CONFIG);
node.putString(ACTIVE_BUILD_CONFIG_KEY, currentBuildConfig.getId());
}
}
private void saveAvailableBuildConfigurations(XMLMemento root) {
// Older versions of the project file format do not have build configs,
// so we save a marker denoting that we do.
root.putBoolean(BUILD_CONFIGS_SUPPORTED, isBuildConfigurationsSupported);
for (String cfgId : getBuildConfigurations()) {
IMemento node = root.createChild(BUILD_CONFIG);
node.putString(BUILD_CONFIG_ID_KEY, cfgId);
IBuildConfiguration cfg = getBuildConfiguration(cfgId);
node.putString(
BUILD_CONFIG_TYPES,
PropertyUtil.fromStrings(cfg.getTypes().toArray(
new String[0])));
}
}
private void saveProperties(IMemento memento, Map<String, String> properties) {
TreeMap<String, String> sortedProperties = new TreeMap<String, String>(
String.CASE_INSENSITIVE_ORDER);
sortedProperties.putAll(properties);
for (Iterator<String> propertyIterator = sortedProperties.keySet()
.iterator(); propertyIterator.hasNext();) {
String key = propertyIterator.next();
String value = properties.get(key);
IMemento property = memento.createChild(PROPERTY);
property.putString(KEY_KEY, key);
property.putString(VALUE_KEY, value);
}
}
private void saveTargetProfile(IMemento memento) {
if (target != null) {
memento.putString(VENDOR_KEY, target.getVendor().getName());
memento.putString(PROFILE_KEY, target.getName());
}
}
public IPath getMoSyncProjectMetaDataLocation(int store) {
switch (store) {
case SHARED_PROPERTY:
return project.getLocation().append(
MOSYNC_PROJECT_META_DATA_FILENAME);
case LOCAL_PROPERTY:
return project.getLocation().append(
MOSYNC_LOCAL_PROJECT_META_DATA_FILENAME);
case WORKSPACE_LOCAL_PROPERTY:
return project
.getLocation()
.append(MOSYNC_LOCAL_PROJECT_META_DATA_FILENAME + "."
+ CoreMoSyncPlugin.getDefault().getWorkspaceToken());
default:
throw new IllegalArgumentException();
}
}
/**
* <p>
* Returns a (shared) instance of a MoSyncProject of the provided project,
* or <code>null</code> if the project does not have a MoSync nature.
* </p>
*
* @param project
* The eclipse project this <code>MoSyncProject</code> should
* wrap
* @return
*/
public static MoSyncProject create(IProject project) {
try {
if (!MoSyncNature.isCompatible(project)) {
return null;
}
boolean upgrade = false;
MoSyncProject result = null;
synchronized (projects) {
result = projects.get(project);
if (result == null) {
result = new MoSyncProject(project);
projects.put(project, result);
upgrade = !CURRENT_VERSION
.equals(result.getFormatVersion());
}
}
if (upgrade) {
upgrade(result);
}
return result;
} catch (CoreException e) {
return null;
}
}
/**
* Extracts all {@code MoSyncProject}s that contains any one of the
* provided {@code IResource}s.
* @param projects
* @return A list of {@code MoSyncProject}s, which may be empty but never {@code null}
*/
public static List<MoSyncProject> create(List<IResource> resources) {
ArrayList<MoSyncProject> result = new ArrayList<MoSyncProject>();
HashSet<IProject> alreadyCreated = new HashSet<IProject>();
for (IResource resource : resources) {
IProject project = resource.getProject();
if (!alreadyCreated.contains(project)) {
alreadyCreated.add(project);
MoSyncProject mosyncProject = MoSyncProject.create(project);
if (project != null) {
result.add(mosyncProject);
}
}
}
return result;
}
private static void upgrade(MoSyncProject project) throws CoreException {
// TODO: Whenever the need arises we may want to fix something smarter
MoSyncProjectConverter1_2.getInstance().convert(project);
MoSyncProjectConverter1_4.getInstance().convert(project);
MoSyncProjectConverter1_7.getInstance().convert(project);
project.setFormatVersion(CURRENT_VERSION);
}
public static void addCapabilityFilters(MoSyncProject result, String[] requiredCapabilities) {
if (result.getProfileManagerType() == MoSyncTool.DEFAULT_PROFILE_TYPE) {
result.getDeviceFilter().addFilter(
DeviceCapabilitiesFilter.create(requiredCapabilities, new String[0]));
}
}
public static void addDefaultResourceFilter(IProject project,
IProgressMonitor monitor) throws CoreException {
// TODO: Hmmm.... maybe we should consider filtering out output folders?
// FileInfoMatcherDescription filter = new
// FileInfoMatcherDescription("org.eclipse.core.resources.regexFilterMatcher",
// ".*rebuild.build.cpp");
// Very internal format, but the version number should help us be a bit
// future proof.
FileInfoMatcherDescription filter = new FileInfoMatcherDescription(
"org.eclipse.ui.ide.multiFilter",
"1.0-name-matches-false-true-.*rebuild.build.cpp");
IResourceFilterDescription created = project.createFilter(
IResourceFilterDescription.EXCLUDE_ALL
| IResourceFilterDescription.FILES, filter, 0, monitor);
}
/**
* Disposes of this mosyncproject, so subsequent calls to
* <code>MoSyncProject.create(IProject)</code> will return another
* <code>MoSyncProject</code> object. Clients can, but should not, perform
* operations on a disposed mosyncproject.
*/
public void dispose() {
synchronized (projects) {
projects.remove(this.project);
}
disposed = true;
}
public boolean isDisposed() {
return disposed;
}
/**
* <p>
* Returns a (shared) instance of a MoSyncProject of the provided project,
* or <code>null</code> if the project does not have a MoSync nature.
* </p>
* <p>
* An optional project meta data file initialization file may be provided.
* This file will then be loaded and its project meta data used by the
* project.
* </p>
*
* @param project
* The eclipse project this <code>MoSyncProject</code> should
* wrap
* @param projectMetadataLocation
* The location of the (shared) project meta data. If
* <code>null</code>, no initialization will take place unless
* this is the first call to <code>create</code> with the
* provided <code>project</code>.
* @return
*/
public static MoSyncProject create(IProject project,
IPath projectMetadataLocation) {
MoSyncProject result = create(project);
if (projectMetadataLocation != null) {
// We only imported SHARED properties.
result.initFromProjectMetaData(projectMetadataLocation,
SHARED_PROPERTY);
// ... but MOSYNCTWOSIX-344 showed we need the local
// ones too
IPath localMetaDataLocation = projectMetadataLocation
.removeLastSegments(1).append(
MOSYNC_LOCAL_PROJECT_META_DATA_FILENAME);
result.initFromProjectMetaData(localMetaDataLocation,
LOCAL_PROPERTY);
}
result.updateProjectSpec();
return result;
}
/**
* <p>
* Returns a (shared) instance of a MoSyncProject that has a given name, or
* <code>null</code> if no such project exists.
* </p>
*/
public static MoSyncProject create(String name) {
IProject project = ResourcesPlugin.getPlugin().getWorkspace().getRoot()
.getProject(name);
return project == null ? null : create(project);
}
/**
* <p>
* Returns the current target profile of this MoSync project.
* </p>
* <p>
* If none is set, a default target profile is returned.
* </p>
*/
@Override
public IProfile getTargetProfile() {
return target == null ? getProfileManager().getDefaultTargetProfile() : target;
}
/**
* <p>
* Sets the current target profile of this MoSync project, and notifies all
* listeners about this change.
* </p>
*
* @param newTarget
* The new target profile
*/
public void setTargetProfile(IProfile newTarget) {
IProfile oldTarget = initTargetProfile(newTarget);
firePropertyChange(new PropertyChangeEvent(this,
TARGET_PROFILE_CHANGED, oldTarget, getTargetProfile()));
}
/**
* Initializes the target profile; same as <code>setTargetProfile</code>,
* but no event is fired.
*
* @param newTarget
* @return The old target profile.
*/
public IProfile initTargetProfile(IProfile newTarget) {
IProfile oldTarget = getTargetProfile();
this.target = newTarget;
updateProjectSpec();
return oldTarget;
}
private void firePropertyChange(PropertyChangeEvent event) {
// TODO: A bit out of place, but it works
excludes.clear();
try {
globalListeners.firePropertyChange(event);
listeners.firePropertyChange(event);
} catch (Exception e) {
// Continue anyway.
CoreMoSyncPlugin.getDefault().log(e);
}
}
/**
* <p>
* Adds a <emph>global</emph> property listener, ie a listener that listens
* to changes to <emph>all</emph> MoSync projects in a workspace.
* </p>
*
* @param globalListener
* The listener to add
*/
public static void addGlobalPropertyChangeListener(
PropertyChangeListener globalListener) {
globalListeners.addPropertyChangeListener(globalListener);
}
/**
* <p>
* Removes a <emph>global</emph> property listener, ie a listener that
* listens to changes to <emph>all</emph> MoSync projects in a workspace.
* </p>
*
* @param globalListener
* The listener to remove
*/
public static void removeGlobalPropertyChangeListener(
PropertyChangeListener globalListener) {
globalListeners.removePropertyChangeListener(globalListener);
}
/**
* <p>
* Adds a property listener to this project.
* </p>
* <p>
* Property listeners are notified about events such as changing target
* profile or any other project property change set by
* <code>setProperty</code>.
*
* @param globalListener
* The listener to add
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
listeners.addPropertyChangeListener(listener);
}
/**
* <p>
* Removes a property listener from this project.
* </p>
*
* @param globalListener
* The listener to remove
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
listeners.removePropertyChangeListener(listener);
}
/**
* <p>
* Returns the <code>ICompositeDeviceFilter</code> currently associated with
* this project.
* </p>
*
* @return
*/
public ICompositeDeviceFilter getDeviceFilter() {
return deviceFilter;
}
/**
* <p>
* Sets the <code>ICompositeDeviceFilter</code> currently associated with
* this project.
* </p>
*
* @param deviceFilter
* The new filter
*/
public void setDeviceFilter(ICompositeDeviceFilter deviceFilter) {
ICompositeDeviceFilter oldFilter = this.deviceFilter;
removeDeviceFilterListener();
this.deviceFilter = deviceFilter;
addDeviceFilterListener();
firePropertyChange(new PropertyChangeEvent(this,
IDeviceFilter.FILTER_CHANGED, oldFilter, deviceFilter));
}
/**
* <p>
* Returns the path to the STABS debug info file of a specific build
* configuration.
* </p>
*
* @param buildConfiguration
* @return
*/
public IPath getStabsPath(IBuildConfiguration buildConfiguration) {
IPath outputPath = MoSyncBuilder.getOutputPath(
project,
new BuildVariant(getTargetProfile(), buildConfiguration == null ? null
: buildConfiguration.getId())).append("stabs.tab");
return outputPath;
}
/**
* Returns the SLD for a specific buildconfiguration; if a null build
* configuration is passed as argument, then this amounts to build
* configurations not being supported.
*
* @param buildConfiguration
* @return
*/
public synchronized SLD getSLD(IBuildConfiguration buildConfiguration) {
IPath outputPath = MoSyncBuilder.getOutputPath(
project,
new BuildVariant(getTargetProfile(), buildConfiguration == null ? null
: buildConfiguration.getId())).append("Sld.tab");
SLD sld = slds.get(outputPath.toPortableString());
if (sld == null) {
sld = new SLD(this, outputPath);
slds.put(outputPath.toPortableString(), sld);
}
return sld;
}
/**
* Returns the underlying Eclipse project of this MoSync project.
*
* @return
*/
public IProject getWrappedProject() {
return project;
}
/**
* <p>
* Returns a project-specific property, using default properties set by
* property initializers if necessary.
* </p>
*/
@Override
public String getProperty(String key) {
String property = properties.get(key);
if (property == null) {
return getDefaultProperty(key);
}
return property;
}
/**
* <p>
* Returns all properties of this project, including properties for all
* build configurations.
* </p>
* <p>
* This method will return a copy of the properties WITHOUT default values,
* so the map returned may be freely modified by clients
* </p>
* <p>
* <b>NOTE:</b> Are you sure you want to use this method? You probably want
* to use getPropertyOwner(), which will properly handle build
* configurations, etc.</code>
*/
@Override
public Map<String, String> getProperties() {
return properties.toMap();
}
/**
* <p>
* Sets a project-specific property.
* </p>
*
* @param key
* @param value
* @return <code>true</code> if and only if the property was changed
*/
@Override
public boolean setProperty(String key, String value) {
String oldValue = getProperty(key);
if (Util.equals(oldValue, value)) {
return false;
}
initProperty(key, value);
updateProjectSpec();
firePropertyChange(new PropertyChangeEvent(this, key, oldValue, value));
return true;
}
/**
* <p>
* Returns the list of vendors to use for this project, using the device
* filter for this project.
* </p>
* <p>
* To get a list of all vendors, use MoSyncTool.getVendors.
* </p>
*
* @return
*/
public IVendor[] getFilteredVendors() {
IVendor[] allVendors = getProfileManager().getVendors();
return AbstractDeviceFilter.filterVendors(allVendors, deviceFilter);
}
public ProfileManager getProfileManager() {
return MoSyncTool.getDefault().getProfileManager(
getProfileManagerType());
}
public int getProfileManagerType() {
int mgrType = PropertyUtil.getInteger(this, PROFILE_MANAGER_TYPE_KEY,
MoSyncTool.LEGACY_PROFILE_TYPE);
return mgrType;
}
/**
* Sets the profile manager type of this project, and if it
* is different from it's current type: makes
* sure to change the target profile to a default one as well
* as setting all filters to a reasonable value. (This is actually more of a conversion
* method than a setter).
* @param type
*/
public void setProfileManagerType(int type) {
setProfileManagerType(type, false);
}
public void setProfileManagerType(int type, boolean force) {
if (force || getProfileManagerType() != type) {
PropertyUtil.setInteger(this, MoSyncProject.PROFILE_MANAGER_TYPE_KEY, type);
getDeviceFilter().removeAllFilters();
addCapabilityFilters(this, createCapabilitiesFromPermissions());
setTargetProfile(null);
}
}
private String[] createCapabilitiesFromPermissions() {
IApplicationPermissions permissions = getPermissions();
List<String> permissionNames = permissions.getRequestedPermissions(true);
TreeSet<String> availableCapabilities = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
availableCapabilities.addAll(
Arrays.asList(ProfileDBManager.getInstance().getAvailableCapabilities(true)));
ArrayList<String> result = new ArrayList<String>();
for (String permission : permissionNames) {
// We only support one-level permissions as of now
if (availableCapabilities.contains(permission) && permission.indexOf('/') == -1) {
result.add(permission);
}
}
return result.toArray(new String[0]);
}
/**
* <p>
* Returns the list of profiles to use for this project, using the device
* filter associated with this project.
* </p>
* <p>
* To get a list of all profiles, use MoSyncTool.getProfiles.
* </p>
*
* @return
*/
public IProfile[] getFilteredProfiles() {
IProfile[] profiles = getProfileManager().getProfiles(deviceFilter);
return profiles;
}
public IProfile[] getFilteredProfiles(IVendor vendor) {
return ProfileManager.filterProfiles(vendor.getProfiles(), deviceFilter);
}
/**
* Returns the default value of a project specific property
*/
@Override
public String getDefaultProperty(String key) {
return CoreMoSyncPlugin.getDefault().getDefaultValue(this, key);
}
@Override
public boolean isDefault(String key) {
return properties.get(key) == null;
}
/**
* <p>
* Sets the property, regardless of any previous value.
* </p>
* <p>
* Any property set by this method will be stored in the SHARED_PROPERTY
* properties.
* </p>
*
* @param key
* @param value
* If <code>null</code>, then the entry is removed
*/
@Override
public void initProperty(String key, String value) {
initProperty(key, value, getStoreForKey(key), false);
}
private int getStoreForKey(String key) {
// As of this moment, only workspace local or shared...
return key.endsWith(SecureProperties.DEFAULT_SECURE_PROPERTY_SUFFIX) ? WORKSPACE_LOCAL_PROPERTY
: SHARED_PROPERTY;
}
/**
* <p>
* Sets the property, regardless of any previous value.
* </p>
*
* @param key
* @param value
* If <code>null</code>, then the entry is removed
* @param store
* The store that this key should be set in, ie LOCAL, SHARED or
* WORKSPACE_LOCAL
*/
public void initProperty(String key, String value, int store, boolean save) {
if (value == null) {
getProperties(store).remove(key);
} else {
getProperties(store).put(key, value);
}
if (save) {
updateProjectSpec(store);
}
}
private Map<String, String> getProperties(int store) {
switch (store) {
case SHARED_PROPERTY:
return sharedProperties;
case LOCAL_PROPERTY:
return localProperties;
case WORKSPACE_LOCAL_PROPERTY:
return workspaceLocalProperties;
default:
throw new IllegalArgumentException();
}
}
/**
* A convenience method for returning the exclusion filter for a
* project/config.
*
* @param project
* The project for which to find the filter
* @param withStandardExcludes
* Whether to use the standard excludes of the project when
* computing the filter
* @return
*/
public static PathExclusionFilter getExclusionFilter(MoSyncProject project,
boolean withStandardExcludes) {
if (project == null) {
return null;
}
IPropertyOwner properties = project.getPropertyOwner();
PathExclusionFilter result = withStandardExcludes ? project.excludes
.get(properties) : null;
if (result == null) {
// TODO: Efficient - well, not really... And this method is heavily
// used!
String[] standardExclusion = withStandardExcludes ? PropertyUtil
.getStrings(properties, STANDARD_EXCLUDES_FILTER_KEY)
: new String[0];
String[] exclusions = PropertyUtil.getStrings(properties,
EXCLUDE_FILTER_KEY);
String[] aggregateExclusions = new String[standardExclusion.length
+ exclusions.length];
System.arraycopy(standardExclusion, 0, aggregateExclusions, 0,
standardExclusion.length);
System.arraycopy(exclusions, 0, aggregateExclusions,
standardExclusion.length, exclusions.length);
Map<String, String> params = properties.getProperties();
ArrayList<String> finalExclusions = new ArrayList<String>();
// TODO: All params should be able to have % tags.
for (int i = 0; i < aggregateExclusions.length; i++) {
String excluded = Util.replace(aggregateExclusions[i], params);
IPath[] excludedPaths = PropertyUtil.toPaths(excluded);
String[] excludedPathsStr = new String[excludedPaths.length];
for (int j = 0; j < excludedPathsStr.length; j++) {
excludedPathsStr[j] = excludedPaths[j].toPortableString();
}
finalExclusions.addAll(Arrays.asList(excludedPathsStr));
}
result = PathExclusionFilter.parse(finalExclusions
.toArray(new String[0]));
if (withStandardExcludes) {
project.excludes.put(properties, result);
}
}
return result;
}
public static void setExclusionFilter(MoSyncProject project,
PathExclusionFilter filter) {
PropertyUtil.setStrings(project.getPropertyOwner(), EXCLUDE_FILTER_KEY,
filter.getFileSpecs());
}
@Override
public String getContext() {
return CONTEXT;
}
/**
* Returns the name of this MoSync project.
*
* @return
*/
public String getName() {
return getWrappedProject().getName();
}
/**
* <p>
* Adds all properties of <code>properties</code> to this project.
* </p>
* <p>
* They will all be added to the SHARED store.
* </p>
*
* @param properties
*/
public void setProperties(Map<String, String> properties) {
sharedProperties.putAll(properties);
updateProjectSpec();
invalidatePropertyDependentObjects();
}
/**
* <p>
* Returns the build state for a variant manager of this project. All
* non-finalizer build states are cached.
* </p>
*
* @return
*/
public IBuildState getBuildState(IBuildVariant variant) {
IBuildState result = cachedBuildStates.get(variant);
boolean wasNull = true; // result == null;
if (wasNull) {
result = new BuildState(this, variant);
}
if (wasNull) {
cachedBuildStates.put(variant, result);
}
return result;
}
/**
* Returns the current build configuration. If none is assigned, this method
* will try to assign a default build configuration (
* <code>IBuildConfiguration.RELEASE_ID</code>). If the default build
* configuration does not exist, <code>null</code> will be returned. This
* method will return a value regardless of what
* <code>isBuildConfigurationsSupported</code> returns.
*
* @return
*/
public IBuildConfiguration getActiveBuildConfiguration() {
if (currentBuildConfig == null) {
currentBuildConfig = getBuildConfiguration(IBuildConfiguration.RELEASE_ID);
}
return currentBuildConfig;
}
/**
* Sets the current build configuration. A property change event with event
* type <code>BUILD_CONFIGURATION_CHANGED</code> is triggered.
*
* @param id
* The new build configuration to use
* @throws IllegalArgumentException
* If no build configuration with the given id exists.
*/
public void setActiveBuildConfiguration(String id) {
IBuildConfiguration oldCfg = getActiveBuildConfiguration();
Object oldId = oldCfg == null ? null : oldCfg.getId();
IBuildConfiguration newConfiguration = getBuildConfiguration(id);
if (newConfiguration == null) {
throw new IllegalArgumentException(MessageFormat.format(
"No configuration with id {0}", id));
}
if (!id.equals(oldId)) {
currentBuildConfig = newConfiguration;
updateProjectSpec();
firePropertyChange(new PropertyChangeEvent(this,
BUILD_CONFIGURATION_CHANGED, oldId, id));
}
}
public Set<String> getBuildConfigurations() {
return new TreeSet<String>(configurations.keySet());
}
public Set<IBuildConfiguration> getBuildConfigurations(
Collection<String> ids) {
TreeSet<IBuildConfiguration> configs = new TreeSet<IBuildConfiguration>(
BuildConfiguration.DEFAULT_COMPARATOR);
for (String id : ids) {
configs.add(getBuildConfiguration(id));
}
return configs;
}
/**
* Returns the ids of all build configurations that has a set of types
*
* @param types
* The types to match against
* @return The build configurations that have <b>all</b> the specified
* types, sorted in case-insensitive alphabetical order.
*/
public SortedSet<String> getBuildConfigurationsOfType(String... types) {
TreeSet<String> result = new TreeSet<String>(
String.CASE_INSENSITIVE_ORDER);
for (IBuildConfiguration cfg : configurations.values()) {
boolean doAdd = true;
for (int i = 0; i < types.length; i++) {
doAdd &= cfg.getTypes().contains(types[i]);
}
if (doAdd) {
result.add(cfg.getId());
}
}
return result;
}
/**
* Returns the build configuration for a given id
*
* @param id
* @return <code>null</code> if the given id is <code>null</code> or if
* there is no build configuration with that id
*/
public IBuildConfiguration getBuildConfiguration(String id) {
if (id == null) {
return null;
}
return configurations.get(id);
}
public IBuildConfiguration installBuildConfiguration(String id,
String[] types) {
BuildConfiguration newConfig = new BuildConfiguration(this, id, types);
installBuildConfiguration(newConfig);
return newConfig;
}
public void installBuildConfiguration(IBuildConfiguration newConfig) {
String id = newConfig.getId();
this.configurations.put(id, newConfig);
updateProjectSpec();
firePropertyChange(new PropertyChangeEvent(this,
BUILD_CONFIGURATION_CHANGED, null, newConfig));
}
public void deinstallBuildConfiguration(String id) {
IBuildConfiguration removed = this.configurations.remove(id);
removed.getProperties().clear();
if (currentBuildConfig == removed) {
currentBuildConfig = null;
if (!configurations.isEmpty()) {
Entry<String, IBuildConfiguration> entry = configurations
.lowerEntry(id);
currentBuildConfig = entry == null ? configurations
.firstEntry().getValue() : entry.getValue();
}
}
updateProjectSpec();
listeners
.firePropertyChange(BUILD_CONFIGURATION_CHANGED, removed, null);
}
public boolean setBuildConfigurationsSupported(
boolean isBuildConfigurationsSupported) {
if (this.isBuildConfigurationsSupported != isBuildConfigurationsSupported) {
this.isBuildConfigurationsSupported = isBuildConfigurationsSupported;
updateProjectSpec();
listeners.firePropertyChange(BUILD_CONFIGURATION_SUPPORT_CHANGED,
!isBuildConfigurationsSupported,
isBuildConfigurationsSupported);
return true;
}
return false;
}
public boolean areBuildConfigurationsSupported() {
return isBuildConfigurationsSupported;
}
/**
* Activates build configurations for this project. If there already are
* installed build configurations, this amounts to calling
* <code>setBuildConfigurationsSupported(true);</code>, otherwise a default
* set of build configurations are installed.
*/
public void activateBuildConfigurations() {
setBuildConfigurationsSupported(true);
if (configurations.isEmpty()) {
configurations.put("Release", new BuildConfiguration(this,
IBuildConfiguration.RELEASE_ID,
IBuildConfiguration.RELEASE_TYPE));
configurations.put("Debug", new BuildConfiguration(this,
IBuildConfiguration.DEBUG_ID,
IBuildConfiguration.DEBUG_TYPE));
setActiveBuildConfiguration("Release");
}
}
public IPropertyOwner getPropertyOwner() {
if (isBuildConfigurationsSupported && currentBuildConfig != null) {
return currentBuildConfig.getProperties();
}
return this;
}
/**
* Returns the secure property owner of this project.
*
* @return
*/
public ISecurePropertyOwner getSecurePropertyOwner() {
return securePropertyOwner;
}
public LibraryLookup getLibraryLookup(IBuildVariant variant, IPropertyOwner buildProperties) {
// TODO: cache?
return new LibraryLookup(MoSyncBuilder.getLibraryPaths(
getWrappedProject(), buildProperties),
MoSyncBuilder.getLibraries(this, variant, buildProperties));
}
public IApplicationPermissions getPermissions() {
return permissions;
}
/**
* Returns the icon file associated with this project.
*
* @return the icon file associated with this project, null if no icon file
* exists.
*/
public File getIconFile() {
return findIconFile(getWrappedProject().getLocation().toFile());
}
/**
* Recursive search for a file that ends with
* DefaultPackager.ICON_FILE_EXTENSION.
*
* @param rootFile
* The root directory to begin searching in.
* @return a file on success and null if no such file was found.
*/
private File findIconFile(final File rootFile) {
if (rootFile.isDirectory()) {
final File[] childs = rootFile.listFiles();
for (File child : childs) {
File iconFile = findIconFile(child);
if (iconFile != null) {
IFile[] iconResource = getWrappedProject().getWorkspace()
.getRoot()
.findFilesForLocationURI(iconFile.toURI());
if (iconResource.length > 0
&& getExclusionFilter(this, true).accept(
iconResource[0])) {
return iconFile;
}
}
}
return null;
}
if (rootFile.getName().endsWith(ICON_FILE_EXTENSION) == true) {
return rootFile;
}
return null;
}
/**
* Returns the version of the format used to persist the project meta data.
*
* @return
*/
public Version getFormatVersion() {
return formatVersion;
}
public void setFormatVersion(Version formatVersion) {
this.formatVersion = formatVersion;
updateProjectSpec();
}
/**
* Returns a list of the names of all open projects that are compatible
* {@link MoSyncProject}s
*
* @see {@link MoSyncNature#isCompatible(IProject)}
* @return
* @throws CoreException
*/
public static List<String> listAllProjects() {
ArrayList<String> result = new ArrayList<String>();
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot()
.getProjects();
for (IProject project : allProjects) {
try {
if (project.isOpen() && MoSyncNature.isCompatible(project)) {
result.add(project.getName());
}
} catch (CoreException e) {
// Should only happen if project is not open
// and we already check for this - hence this is
// a runtime exception
throw new RuntimeException(e);
}
}
return result;
}
public String getOutputType() {
return getProperty(MoSyncBuilder.OUTPUT_TYPE);
}
/**
* Sets the (preferred) output type of this project.
* Will throw an exception with a detailed, user-friendly
* message if this operation cannot be performed.
* Use {@link #forceOutputType(String)} to ignore any
* exceptions -- this will cause changes to the project
* that will make it possible to build natively but
* may also make the project un-buildable. (This is by design)
* @param binaryType
* @return
* @throws IllegalArgumentException
*/
public boolean setOutputType(String binaryType) throws IllegalArgumentException {
ArrayList<String> errors = new ArrayList<String>();
if (MoSyncBuilder.OUTPUT_TYPE_NATIVE_COMPILE.equals(binaryType)) {
if (getProfileManagerType() != MoSyncTool.DEFAULT_PROFILE_TYPE) {
errors.add("Native projects must make use of the platform based profile type.");
}
if (!isBuildConfigurationsSupported) {
errors.add("Native projects require build configuration support");
}
boolean changedIncludes = false;
for (String cfg : getBuildConfigurations()) {
Pair<Boolean, List<IPath>> nativePaths = filteredNativePaths(cfg);
changedIncludes |= nativePaths.first;
}
if (changedIncludes) {
errors.add("Native projects only supports include paths relative to the project.");
}
}
if (errors.isEmpty()) {
return forceOutputType(binaryType);
} else {
String errorMsg = " * " + Util.join(errors.toArray(), "\n * ");
throw new IllegalArgumentException(errorMsg);
}
}
private Pair<Boolean, List<IPath>> filteredNativePaths(String cfg) {
return MoSyncBuilder.filterNativeIncludePaths(this, new BuildVariant(getTargetProfile(), cfg));
}
public boolean forceOutputType(String binaryType) {
boolean result = false;
if (MoSyncBuilder.OUTPUT_TYPE_NATIVE_COMPILE.equals(binaryType)) {
result = setBuildConfigurationsSupported(true);
setProfileManagerType(MoSyncTool.DEFAULT_PROFILE_TYPE);
for (String cfg : getBuildConfigurations()) {
Pair<Boolean, List<IPath>> nativePaths = filteredNativePaths(cfg);
PropertyUtil.setPaths(getBuildConfiguration(cfg).getProperties(),
MoSyncBuilder.ADDITIONAL_NATIVE_INCLUDE_PATHS, nativePaths.second.toArray(new IPath[0]));
}
}
result |= setProperty(MoSyncBuilder.OUTPUT_TYPE, binaryType);
return result;
}
}