/* 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.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import com.mobilesorcery.sdk.internal.StringMatcher;
public class PathExclusionFilter implements IFilter<IResource> {
private final static int NEUTRAL = 0;
private final static int INCLUDE = 1;
private final static int EXCLUDE = -INCLUDE;
static class InternalFilter {
private String filespec;
private StringMatcher matcher;
private boolean exclude;
public InternalFilter(String filespec, boolean exclude) {
this.filespec = filespec;
Path filePath = new Path(filespec);
this.matcher = new StringMatcher(filePath.toPortableString(), true, false);
this.exclude = exclude;
}
private InternalFilter(InternalFilter prototype) {
this(prototype.filespec, prototype.exclude);
}
public int accept(IResource resource) {
IResource directoryLocation = resource.getProject().findMember(new Path(filespec));
boolean isDirectoryFilter = directoryLocation != null && directoryLocation.getLocation().toFile().isDirectory();
boolean match = matcher.match(resource.getProjectRelativePath().toPortableString());
if (match) {
return exclude ? EXCLUDE : INCLUDE;
} else if (isDirectoryFilter && resource.getType() != IResource.PROJECT && resource.getType() != IResource.ROOT) {
return accept(resource.getParent());
}
return NEUTRAL;
}
public static InternalFilter parse(String filespec) {
if (filespec.charAt(0) == '+') {
return parse(filespec.substring(1)).inverse();
} else if (filespec.charAt(0) == '-') {
return parse(filespec.substring(1));
}
return new InternalFilter(filespec, true);
}
public InternalFilter inverse() {
InternalFilter copy = new InternalFilter(this);
copy.exclude = !exclude;
return copy;
}
public boolean equals(Object o) {
if (o instanceof InternalFilter) {
return equals((InternalFilter) o);
}
return false;
}
public boolean equals(InternalFilter filter) {
if (filter == null) {
return false;
}
return exclude == filter.exclude && filespec.equals(filter.filespec);
}
public int hashCode() {
return new Boolean(exclude).hashCode() ^ filespec.hashCode();
}
public String getFileSpec() {
String prefix = exclude ? "" : "+";
return prefix + filespec;
}
public String toString() {
return getFileSpec();
}
}
// Todo: create 'internal filters' instead of string[]
private List<InternalFilter> filters = new ArrayList<InternalFilter>();
PathExclusionFilter(String[] filespecs) {
filters = parseFilters(filespecs, true);
}
private PathExclusionFilter(List<InternalFilter> filters) {
this.filters = normalize(filters);
}
private List<InternalFilter> normalize(List<InternalFilter> filters) {
HashSet<InternalFilter> added = new HashSet<InternalFilter>();
ArrayList<InternalFilter> newFilters = new ArrayList<InternalFilter>();
for (InternalFilter filter : filters) {
// No duplicates, please
if (!added.contains(filter)) {
// And some may cancel each other out
if (added.contains(filter.inverse())) {
newFilters.remove(filter.inverse());
added.remove(filter.inverse());
} else {
newFilters.add(filter);
added.add(filter);
}
}
}
return newFilters;
}
private List<InternalFilter> parseFilters(String[] filespecs, boolean exclude) {
List<InternalFilter> filters = new ArrayList<InternalFilter>();
for (int i = 0; i < filespecs.length; i++) {
if (!Util.isEmpty(filespecs[i])) {
InternalFilter parsedFilter = InternalFilter.parse(filespecs[i]);
if (!exclude) {
parsedFilter = parsedFilter.inverse();
}
filters.add(parsedFilter);
}
}
return normalize(filters);
}
public static PathExclusionFilter parse(String[] filespecs) {
return new PathExclusionFilter(filespecs);
}
public boolean accept(IResource resource) {
return !inverseAccept(resource);
}
public boolean inverseAccept(IResource resource) {
int state = NEUTRAL;
for (int i = 0; i < filters.size(); i++) {
int newState = filters.get(i).accept(resource);
state = newState == NEUTRAL ? state : newState;
}
boolean result = state == EXCLUDE;
return result;
}
public PathExclusionFilter addExclusions(List<String> filespecs, boolean excluded) {
List<InternalFilter> newFilespecs = new ArrayList<InternalFilter>(filters);
newFilespecs.addAll(parseFilters(filespecs.toArray(new String[0]), excluded));
return new PathExclusionFilter(newFilespecs);
}
/**
* <p>Convenience method to set the excluded / included state
* of a set of resources. This method will automatically determine
* which project each resource belongs to and update the corresponding
* exclusion filter (if applicable).</p>
* @param resources
* @param excluded
* @return The number of resources that was actually added to the list of
* excluded/included list; if a filter already filters a resource, it will
* not be added.
*/
public static int setExcluded(List<IResource> resources, boolean excluded) {
int added = 0;
for (IResource resource : resources) {
MoSyncProject project = MoSyncProject.create(resource.getProject());
IPath path = resource.getProjectRelativePath();
PathExclusionFilter filter = MoSyncProject.getExclusionFilter(project, true);
PathExclusionFilter filterWithoutStandardExcludes = MoSyncProject.getExclusionFilter(project, false);
boolean accept = filter.accept(resource);
boolean shouldAdd = excluded ? accept : !accept;
if (shouldAdd) {
added++;
filterWithoutStandardExcludes = filterWithoutStandardExcludes.addExclusions(Arrays.asList(path.toPortableString()), excluded);
MoSyncProject.setExclusionFilter(project, filterWithoutStandardExcludes);
}
}
return added;
}
/**
* Returns a string array that can be used to construct
* a new, equals path exclusion filter.
* @return
*/
public String[] getFileSpecs() {
ArrayList<String> result = new ArrayList<String>();
for (InternalFilter filter : filters) {
result.add(filter.getFileSpec());
}
return result.toArray(new String[0]);
}
public String toString() {
return Util.join(getFileSpecs(), " ");
}
}