/* 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.internal;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import com.mobilesorcery.sdk.core.BuildResult;
import com.mobilesorcery.sdk.core.BuildVariant;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.IBuildResult;
import com.mobilesorcery.sdk.core.IBuildState;
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.PropertyUtil;
import com.mobilesorcery.sdk.core.SectionedPropertiesFile;
import com.mobilesorcery.sdk.core.SectionedPropertiesFile.Section;
import com.mobilesorcery.sdk.core.SectionedPropertiesFile.Section.Entry;
import com.mobilesorcery.sdk.core.Util;
import com.mobilesorcery.sdk.internal.dependencies.DependencyManager;
public class BuildState implements IBuildState {
public static class Diff implements IFileTreeDiff {
private final HashSet<IPath> added = new HashSet<IPath>();
private final HashSet<IPath> changed = new HashSet<IPath>();
private final HashSet<IPath> removed = new HashSet<IPath>();
@Override
public Set<IPath> getAdded() {
return added;
}
@Override
public Set<IPath> getChanged() {
return changed;
}
@Override
public Set<IPath> getRemoved() {
return removed;
}
public void markChanged(IPath changed) {
this.changed.add(changed);
}
public void markRemoved(IPath removed) {
this.removed.add(removed);
}
public void markAdded(IPath added) {
this.added.add(added);
}
@Override
public String toString() {
return "ADDED: " + added + "\n" +
"CHANGED: " + changed + "\n" +
"REMOVED: " + removed;
}
@Override
public boolean isEmpty() {
return added.isEmpty() && changed.isEmpty() && removed.isEmpty();
}
}
class FileInfoTree implements IResourceVisitor {
HashMap<IPath, Long> timestampMap = new HashMap<IPath, Long>();
/**
* Computes a diff between this tree and another tree,
* with this tree as the "to" tree, and the other tree
* as the "from" tree. Or, informally: other + diff = this
* @param other
* @return
*/
Diff computeDiff(FileInfoTree other) {
Diff diff = new Diff();
for (IPath path : timestampMap.keySet()) {
Long otherTimestamp = other.timestampMap.get(path);
if (otherTimestamp == null) {
recursiveAdd(diff.added, path);
} else if (otherTimestamp.compareTo(timestampMap.get(path)) != 0) {
recursiveAdd(diff.changed, path);
}
if (CoreMoSyncPlugin.getDefault().isDebugging()) {
Long timeStamp = timestampMap.get(path);
CoreMoSyncPlugin.trace("{0} previous timestamp: {1}, current timestamp: {2}", path, otherTimestamp == null ? "N/A" : new Date(otherTimestamp), timeStamp == null ? "N/A" : new Date(timeStamp));
}
}
for (IPath path : other.timestampMap.keySet()) {
if (!timestampMap.containsKey(path)) {
recursiveAdd(diff.removed, path);
}
}
return diff;
}
private void recursiveAdd(HashSet<IPath> set, IPath path) {
while (!path.isEmpty()) {
set.add(path);
path = path.removeLastSegments(1);
}
}
@Override
public boolean visit(IResource resource) throws CoreException {
internalUpdateResource(resource);
return !resource.isDerived();
}
private void internalUpdateResource(IResource resource) {
if (resource.getType() == IResource.FILE) {
internalUpdateState(resource.getProjectRelativePath());
}
}
private void internalUpdateState(IPath path) {
IResource projectResource = project.getWrappedProject().findMember(path);
/**
* Ignore virtual folders since they do not have a timestamp and
* getLocation always returns null for them.
*/
if(projectResource.isVirtual()) {
return;
}
IPath fullpath = projectResource.getLocation();
File file = fullpath.toFile();
long newTimestamp = file.lastModified();
timestampMap.put(path, newTimestamp);
}
public void removeState(IPath removed) {
timestampMap.remove(removed);
}
}
private FileInfoTree tree;
private DependencyManager<IResource> dependencies;
private IBuildVariant variant;
private MoSyncProject project;
private File buildStateFile;
private IBuildResult buildResult;
private boolean valid = true;
private boolean fullRebuildNeeded;
private Map<String, String> properties;
public BuildState(MoSyncProject project, IBuildVariant variant) {
this.project = project;
this.variant = variant;
IPath metaDataPath = MoSyncBuilder.getMetaDataPath(project, variant);
IPath buildStatePath = metaDataPath.append(".buildstate");
buildStateFile = buildStatePath.toFile();
clear();
load();
}
private BuildState() {
}
/**
* Parses the .metadata/.buildstate file of a directory.
* @param location
* @return
*/
public static IBuildState parseBuildState(IResource location) {
BuildState result = new BuildState();
result.project = MoSyncProject.create(location.getProject());
IPath buildStatePath = location.getLocation().append(".metadata/.buildstate");
result.buildStateFile = buildStatePath.toFile();
result.clear();
result.load();
return result.buildStateFile.exists() && result.project != null ? result : null;
}
public static boolean hasBuildState(IResource location) {
IPath buildStatePath = location.getLocation().append(".metadata/.buildstate");
return buildStatePath.toFile().exists();
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#load()
*/
@Override
public void load() {
valid = buildStateFile.exists();
if (valid) {
try {
parseBuildStateFile(buildStateFile);
} catch (Exception e) {
CoreMoSyncPlugin.getDefault().log(e);
valid = false;
// We just consider it a 'fresh' build [state].
}
}
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#isValid()
*/
@Override
public boolean isValid() {
return valid;
}
private void parseBuildStateFile(File buildStateFile) throws IOException {
SectionedPropertiesFile props = SectionedPropertiesFile.parse(buildStateFile);
Section resultSection = props.getDefaultSection();
parseBuildResult(resultSection);
Section files = props.getFirstSection("files");
parseFileState(files);
Section dependenciesSection = props.getFirstSection("dependencies");
parseDependencies(dependenciesSection);
Section buildPropertiesSection = props.getFirstSection("build-properties");
parseBuildProperties(buildPropertiesSection);
}
private void parseBuildProperties(Section buildPropertiesSection) {
if (buildPropertiesSection == null) {
return;
}
properties = buildPropertiesSection.getEntriesAsMap();
}
private void parseDependencies(Section dependenciesSection) {
if (dependenciesSection == null) {
return;
}
List<Entry> dependencyEntries = dependenciesSection.getEntries();
for (Entry dependencyEntry : dependencyEntries) {
IPath dependeePath = new Path(dependencyEntry.getKey());
IPath[] dependencyPaths = PropertyUtil.toPaths(dependencyEntry.getValue());
// Ehm... TODO: Dependencies should be on absolute paths, not resources...
IWorkspaceRoot wr = ResourcesPlugin.getWorkspace().getRoot();
//IFile[] dependeeFiles = wr.findFilesForLocation(dependeePath);
IResource dependeeFile = wr.findMember(dependeePath);
for (int j = 0; j < dependencyPaths.length; j++) {
IResource dependencyFile = wr.findMember(dependencyPaths[j]);
dependencies.addDependency(dependeeFile, dependencyFile);
}
}
}
private void parseFileState(Section files) {
if (files == null) {
return;
}
List<Entry> entries = files.getEntries();
for (Entry entry : entries) {
IPath path = new Path(entry.getKey());
long timestamp = Long.parseLong(entry.getValue());
tree.timestampMap.put(path, timestamp);
}
}
private void parseBuildResult(Section resultSection) {
if (resultSection == null) {
return;
}
Map<String, String> resultMap = resultSection.getEntriesAsMap();
BuildResult buildResult = new BuildResult(project.getWrappedProject());
IBuildVariant variant = BuildVariant.parse(resultMap.get("variant"));
buildResult.setVariant(variant);
this.variant = variant;
buildResult.setSuccess(Boolean.parseBoolean(resultMap.get("success")));
fullRebuildNeeded = Boolean.parseBoolean(resultMap.get("rebuild-needed"));
String timestampStr = resultMap.get("timestamp");
if (timestampStr != null) {
buildResult.setTimestamp(Long.parseLong(timestampStr));
}
for (Map.Entry<String, String> outputs : resultMap.entrySet()) {
String key = outputs.getKey();
String[] filenames = PropertyUtil.toStrings(outputs.getValue());
if (key.startsWith("output")) {
String outputType = key.length() > "output".length() + 1 ?
key.substring("output-".length()) :
null;
File[] files = new File[filenames.length];
for (int i = 0; i < files.length; i++) {
files[i] = new File(filenames[i]);
}
buildResult.setBuildResult(outputType, files);
}
}
this.buildResult = buildResult;
}
@Override
public void setValid(boolean valid) {
this.valid = valid;
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#save()
*/
@Override
public void save() {
FileWriter buildStateWriter = null;
try {
buildStateFile.getParentFile().mkdirs();
buildStateWriter = new FileWriter(buildStateFile);
SectionedPropertiesFile props = SectionedPropertiesFile.create();
Section resultSection = props.getDefaultSection();
saveBuildResult(resultSection);
Section files = props.addSection("files");
saveFileState(files);
Section deps = props.addSection("dependencies");
saveDependencies(deps);
Section buildPropertiesSection = props.addSection("build-properties");
saveBuildProperties(buildPropertiesSection);
buildStateWriter.write(props.toString());
} catch (Exception e) {
CoreMoSyncPlugin.getDefault().log(e);
e.printStackTrace();
// We silently ignore it.
} finally {
Util.safeClose(buildStateWriter);
}
}
private void saveBuildProperties(Section buildPropertiesSection) {
buildPropertiesSection.addEntries(properties);
}
private void saveDependencies(Section deps) {
for (IResource dependee : dependencies.getAllDependees()) {
if (dependee != null) {
IPath dependeePath = dependee.getFullPath();
Set<IResource> dependentResources = dependencies.getDependenciesOf(dependee);
deps.addEntry(new Entry(dependeePath.toPortableString(), PropertyUtil.fromPaths(dependentResources.toArray(new IResource[0]))));
}
}
}
private void saveBuildResult(Section resultSection) {
resultSection.addEntry(new Entry("rebuild-needed", Boolean.toString(fullRebuildNeeded)));
if (buildResult != null) {
resultSection.addEntry(new Entry("variant", BuildVariant.toString(buildResult.getVariant())));
resultSection.addEntry(new Entry("success", Boolean.toString(buildResult.success())));
resultSection.addEntry(new Entry("timestamp", Long.toString(buildResult.getTimestamp())));
Map<String, List<File>> buildArtifacts = buildResult.getBuildResult();
if (buildArtifacts != null) {
for (Map.Entry<String, List<File>> buildArtifact : buildArtifacts.entrySet()) {
String key = buildArtifact.getKey();
String outputKey = "output" + (key == null ? "" : "-" + key);
List<File> files = buildArtifact.getValue();
List<String> filenames = new ArrayList<String>();
for (File file : files) {
filenames.add(file.getAbsolutePath());
}
resultSection.addEntry(new Entry(outputKey, PropertyUtil.fromStrings(filenames.toArray(new String[0]))));
}
}
}
}
private void saveFileState(Section files) {
for (IPath path : tree.timestampMap.keySet()) {
files.addEntry(new Entry(path.toPortableString(), tree.timestampMap.get(path).toString()));
}
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#updateState(org.eclipse.core.resources.IResource)
*/
@Override
public void updateState(IResource resource) throws CoreException {
resource.accept(tree);
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#updateState(com.mobilesorcery.sdk.core.IFileTreeDiff)
*/
@Override
public void updateState(IFileTreeDiff diff) {
for (IPath added : diff.getAdded()) {
tree.internalUpdateState(added);
}
for (IPath changed : diff.getChanged()) {
tree.internalUpdateState(changed);
}
for (IPath removed : diff.getRemoved()) {
tree.removeState(removed);
}
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#updateResult(com.mobilesorcery.sdk.core.IBuildResult)
*/
@Override
public void updateResult(IBuildResult buildResult) {
this.buildResult = buildResult;
}
@Override
public IBuildResult getBuildResult() {
return buildResult;
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#getBuildVariant()
*/
@Override
public IBuildVariant getBuildVariant() {
return variant;
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#fullRebuildNeeded()
*/
@Override
public boolean fullRebuildNeeded() {
return fullRebuildNeeded;
}
@Override
public void fullRebuildNeeded(boolean fullRebuildNeeded) {
this.fullRebuildNeeded = fullRebuildNeeded;
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#getDependencyManager()
*/
@Override
public DependencyManager<IResource> getDependencyManager() {
return dependencies;
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#createDiff()
*/
@Override
public IFileTreeDiff createDiff() throws CoreException {
FileInfoTree currentTree = new FileInfoTree();
IProject project = this.project.getWrappedProject();
project.accept(currentTree);
return currentTree.computeDiff(tree);
}
/* (non-Javadoc)
* @see com.mobilesorcery.sdk.internal.IBuildState#clear()
*/
@Override
public void clear() {
tree = new FileInfoTree();
dependencies = new DependencyManager<IResource>();
properties = new HashMap<String, String>();
buildResult = null;
fullRebuildNeeded = true;
}
@Override
public void updateBuildProperties(Map<String, String> properties) {
this.properties = properties;
}
@Override
public Set<String> getChangedBuildProperties() {
Map<String, String> currentBuildProperties = project.getProperties();
return getPropertiesDiff(properties, currentBuildProperties);
}
public static Set<String> getPropertiesDiff(Map<String, String> oldProperties, Map<String, String> newProperties) {
Set<String> changed = new HashSet<String>();
innerGetPropertiesDiff(changed, oldProperties, newProperties);
innerGetPropertiesDiff(changed, newProperties, oldProperties);
return changed;
}
private static void innerGetPropertiesDiff(Set<String> changed, Map<String, String> oldProperties, Map<String, String> newProperties) {
for (Map.Entry<String, String> entry : newProperties.entrySet()) {
String key = entry.getKey();
String currentValue = entry.getValue();
String oldValue = oldProperties.get(key);
boolean bothEmpty = Util.isEmpty(currentValue) && Util.isEmpty(oldValue);
if (!bothEmpty && !currentValue.equals(oldValue)) {
changed.add(key);
}
}
}
@Override
public Map<String, String> getBuildProperties() {
return Collections.unmodifiableMap(properties);
}
public void updateBuildVariant(IBuildVariant variant) {
this.variant = variant;
}
@Override
public IPath getLocation() {
return new Path(buildStateFile.getParentFile().getAbsolutePath());
}
}