/*
* Copyright (C) 2013 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
*/
package org.robovm.compiler.config;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.robovm.compiler.target.Target;
import org.robovm.compiler.util.AntPathMatcher;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
/**
* Specifies resources needed by the application the compiler produces. A
* resource can be specified using a single {@link File}:
*
* <pre>
* <resource>path/to/the/resource.txt</resource>
* </pre>
*
* If the path specifies a directory the directory including its contents
* (except for the default excludes, see below) will be copied. If the path
* specifies a file, that file will be copied.
* <p>
* A resource be also be specified with a base directory, a target path and
* include and exclude filters (similar to Maven's <resource> element):
*
* <pre>
* <resource>
* <targetPath>data</targetPath>
* <directory>resources</directory>
* <includes>
* <include>**/*</include>
* </includes>
* <excludes>
* <exclude>**/*.bak</exclude>
* </excludes>
* <flatten>false</flatten>
* <ignoreDefaultExcludes>false</ignoreDefaultExcludes>
* <skipPngCrush>false</skipPngCrush>
* </resource>
* </pre>
*
* Each element represents a property in this class. Please see the
* documentation for each property's getter method for more information.
* </p>
* <p>
* The current {@link Target} may transform and rename a resource while being
* copied (e.g. running {@code pngcrush} or converting {@code xib} files to
* {@code nib} files).
* </p>
* <h2><a name="defexcludes">Default excludes</a></h2>
* <p>
* (The same as those used by ANT 1.9)
* </p>
* <p>
* Miscellaneous typical temporary files:
* <ul>
* <li>**/*~</li>
* <li>**/#*#</li>
* <li>**/.#*</li>
* <li>**/%*%</li>
* <li>**/._*</li>
* </ul>
* CVS:
* <ul>
* <li>**/CVS</li>
* <li>**/CVS/**</li>
* <li>**/.cvsignore</li>
* </ul>
* SCCS:
* <ul>
* <li>**/SCCS</li>
* <li>**/SCCS/**</li>
* </ul>
* Visual SourceSafe:
* <ul>
* <li>**/vssver.scc</li>
* </ul>
* Subversion:
* <ul>
* <li>**/.svn</li>
* <li>**/.svn/**</li>
* </ul>
* Git:
* <ul>
* <li>**/.git</li>
* <li>**/.git/**</li>
* <li>**/.gitattributes</li>
* <li>**/.gitignore</li>
* <li>**/.gitmodules</li>
* </ul>
* Mercurial:
* <ul>
* <li>**/.hg</li>
* <li>**/.hg/**</li>
* <li>**/.hgignore</li>
* <li>**/.hgsub</li>
* <li>**/.hgsubstate</li>
* <li>**/.hgtags</li>
* </ul>
* Bazaar:
* <ul>
* <li>**/.bzr</li>
* <li>**/.bzr/**</li>
* <li>**/.bzrignore</li>
* </ul>
* Mac:
* <ul>
* <li>**/.DS_Store</li>
* </ul>
* </p>
*/
public class Resource {
/**
* Interface used by {@link Resource#walk(Walker, File)} to walk the paths
* matched by a {@link Resource}.
*/
public interface Walker {
/**
* Processes the specified folder. This typically does nothing but
* return {@code true} to signal that the files in the folder should be
* processed using {@link #processFile(Resource, File, File)}.
*/
boolean processDir(Resource resource, File dir, File destDir) throws IOException;
/**
* Processes the specified file. This typically copies the file to the
* destination directory possibly renaming/transforming the file in some
* way in the process.
*/
void processFile(Resource resource, File file, File destDir) throws IOException;
}
private static final AntPathMatcher MATCH_ALL_MATCHER = new AntPathMatcher("**/*");
private static final String[] DEFAULTEXCLUDES = {
// Miscellaneous typical temporary files
"**/*~",
"**/#*#",
"**/.#*",
"**/%*%",
"**/._*",
// CVS
"**/CVS",
"**/CVS/**",
"**/.cvsignore",
// SCCS
"**/SCCS",
"**/SCCS/**",
// Visual SourceSafe
"**/vssver.scc",
// Subversion
"**/.svn",
"**/.svn/**",
// Git
"**/.git",
"**/.git/**",
"**/.gitattributes",
"**/.gitignore",
"**/.gitmodules",
// Mercurial
"**/.hg",
"**/.hg/**",
"**/.hgignore",
"**/.hgsub",
"**/.hgsubstate",
"**/.hgtags",
// Bazaar
"**/.bzr",
"**/.bzr/**",
"**/.bzrignore",
// Mac
"**/.DS_Store"
};
private static final List<AntPathMatcher> DEFAULTEXCLUDESMATCHERS;
static {
DEFAULTEXCLUDESMATCHERS = toAntPathMatchers(Arrays.asList(DEFAULTEXCLUDES));
}
@Element(required = false)
private String targetPath;
@Element(required = false)
private File directory;
@ElementList(required = false, entry = "include")
private ArrayList<String> includes;
@ElementList(required = false, entry = "exclude")
private ArrayList<String> excludes;
@Element(required = false)
private Boolean flatten;
@Element(required = false)
private Boolean ignoreDefaultExcludes;
@Element(required = false)
private Boolean skipPngCrush;
@Element(required = false)
private File path;
protected Resource() {}
/**
* Creates a new simple {@link Resource} which will copy the specified file
* or directory.
*
* @param path the {@link File} which will be copied.
*/
public Resource(File path) {
this.path = path;
}
/**
* Creates a new {@link Resource} which will copy all files in the specified
* base directory to the specified target path in the application directory.
*
* @param directory the base directory.
* @param targetPath the target path.
*/
public Resource(File directory, String targetPath) {
this.directory = directory;
this.targetPath = targetPath;
}
/**
* Returns the path which will be copied for simple {@link Resource}s or
* {@code null} if this isn't a simple {@link Resource}.
*
* @return the path.
*/
public File getPath() {
return path;
}
/**
* Returns the target path relative to the application directory (i.e. app
* bundle directory for iOS apps) where paths matching this {@link Resource}
* will be copied. If not specified paths will be copied directly to the
* application directory.
*
* @return the target path or {@code null} if not specified.
*/
public String getTargetPath() {
return targetPath;
}
/**
* Returns the base directory containing the files and directories copied by
* the {@link Resource}.
*
* @return the base directory.
*/
public File getDirectory() {
return directory;
}
/**
* Returns a list of Ant-style patterns (using **, *, ? as wildcards)
* matching files which will be included when copying this {@link Resource}.
*
* @return the include patterns.
* @see AntPathMatcher
*/
public List<String> getIncludes() {
return includes != null ? Collections.unmodifiableList(includes) : Collections.<String> emptyList();
}
/**
* Adds include patterns.
*
* @return {@code this}
* @see #getIncludes()
*/
public Resource include(String... patterns) {
if (includes == null) {
includes = new ArrayList<String>();
}
includes.addAll(Arrays.asList(patterns));
return this;
}
/**
* Returns a list of Ant-style patterns (using **, *, ? as wildcards)
* matching files which will be excluded when copying this {@link Resource}.
*
* @return the exclude patterns.
* @see AntPathMatcher
*/
public List<String> getExcludes() {
return excludes != null ? Collections.unmodifiableList(excludes) : Collections.<String> emptyList();
}
/**
* Adds exclude patterns.
*
* @return {@code this}
* @see #getExcludes()
*/
public Resource exclude(String... patterns) {
if (excludes == null) {
excludes = new ArrayList<String>();
}
excludes.addAll(Arrays.asList(patterns));
return this;
}
/**
* Returns <code>true</code> if the files matched by this {@link Resource}
* should be copied directly into the application directory without
* preserving the directory structure of the source directory. The default
* is <code>false</code>.
*
* @return <code>true</code> if the directory structure should be flattened.
*/
public boolean isFlatten() {
return flatten == null ? false : flatten.booleanValue();
}
/**
* Sets the {@code flatten} property and returns {@code this}.
*
* @param b the new value.
* @return {@code this}
* @see #isFlatten()
*/
public Resource flatten(boolean b) {
this.flatten = b;
return this;
}
/**
* Returns <code>true</code> if the <a href="#defexcludes">default
* excludes</a> should be ignored and copied for this {@link Resource}. The
* default is <code>false</code>, i.e. don't copy files matching the default
* excludes.
*
* @return <code>true</code> if default excludes should be ignored.
*/
public boolean isIgnoreDefaultExcludes() {
return ignoreDefaultExcludes == null ? false : ignoreDefaultExcludes.booleanValue();
}
/**
* Sets the {@code ignoreDefaultExcludes} property and returns {@code this}.
*
* @param b the new value.
* @return {@code this}
* @see #isIgnoreDefaultExcludes()
*/
public Resource ignoreDefaultExcludes(boolean b) {
this.ignoreDefaultExcludes = b;
return this;
}
/**
* Returns <code>true</code> if {@code pngcrush} should not be called for
* PNG files matching this {@link Resource} when targeting iOS. The default
* is <code>false</code>, i.e. {@code pngcrush} WILL be called for PNG
* files.
*
* @return <code>true</code> if {@code pngcrush} should not be called.
*/
public boolean isSkipPngCrush() {
return skipPngCrush == null ? false : skipPngCrush.booleanValue();
}
/**
* Sets the {@code skipPngCrush} property and returns {@code this}.
*
* @param b the new value.
* @return {@code this}
* @see #isSkipPngCrush()
*/
public Resource skipPngCrush(boolean b) {
this.skipPngCrush = b;
return this;
}
public void walk(Walker walker) throws IOException {
walk(walker, new File("."));
}
public void walk(Walker walker, File destDir) throws IOException {
if (targetPath != null && targetPath.trim().length() > 0) {
destDir = new File(destDir, targetPath);
}
if (path != null) {
// Walk path and all its descendants (if a directory) including
// everything except the default excludes.
walk(walker, path.getParentFile(), path.getName(), destDir,
Collections.singletonList(MATCH_ALL_MATCHER), DEFAULTEXCLUDESMATCHERS);
} else {
List<AntPathMatcher> inc = toAntPathMatchers(includes);
if (inc.isEmpty()) {
inc.add(MATCH_ALL_MATCHER);
}
List<AntPathMatcher> exc = toAntPathMatchers(excludes);
if (!isIgnoreDefaultExcludes()) {
exc.addAll(DEFAULTEXCLUDESMATCHERS);
}
File dir = null;
if (directory != null) {
dir = directory.getCanonicalFile();
} else {
dir = new File("").getCanonicalFile();
}
if (dir.exists() && dir.isDirectory()) {
for (File f : dir.listFiles()) {
walk(walker, dir, f.getName(), destDir, inc, exc);
}
}
}
}
private void walk(Walker walker, File baseDir, String path, File destDir,
List<AntPathMatcher> inc, List<AntPathMatcher> exc) throws IOException {
File f = new File(baseDir, path);
// Always descend into directories unless explicitly excluded.
if ((f.isDirectory() || matches(path, inc)) && !matches(path, exc)) {
if (f.isFile()) {
walker.processFile(this, f, destDir);
} else if (f.isDirectory() && walker.processDir(this, f, destDir)) {
File newDestDir = destDir;
if (!isFlatten()) {
newDestDir = new File(destDir, f.getName());
}
for (File child : f.listFiles()) {
walk(walker, baseDir, path + File.separator + child.getName(), newDestDir, inc, exc);
}
}
}
}
private static ArrayList<AntPathMatcher> toAntPathMatchers(List<String> listOfStrings) {
ArrayList<AntPathMatcher> l = new ArrayList<AntPathMatcher>();
if (listOfStrings != null) {
for (String pattern : listOfStrings) {
l.add(new AntPathMatcher(pattern));
}
}
return l;
}
private boolean matches(String path, List<AntPathMatcher> matchers) {
for (AntPathMatcher matcher : matchers) {
if (matcher.matches(path)) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((directory == null) ? 0 : directory.hashCode());
result = prime * result
+ ((excludes == null) ? 0 : excludes.hashCode());
result = prime * result + ((flatten == null) ? 0 : flatten.hashCode());
result = prime
* result
+ ((ignoreDefaultExcludes == null) ? 0 : ignoreDefaultExcludes
.hashCode());
result = prime * result
+ ((includes == null) ? 0 : includes.hashCode());
result = prime * result + ((path == null) ? 0 : path.hashCode());
result = prime * result
+ ((skipPngCrush == null) ? 0 : skipPngCrush.hashCode());
result = prime * result
+ ((targetPath == null) ? 0 : targetPath.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Resource other = (Resource) obj;
if (directory == null) {
if (other.directory != null) {
return false;
}
} else if (!directory.equals(other.directory)) {
return false;
}
if (excludes == null) {
if (other.excludes != null) {
return false;
}
} else if (!excludes.equals(other.excludes)) {
return false;
}
if (flatten == null) {
if (other.flatten != null) {
return false;
}
} else if (!flatten.equals(other.flatten)) {
return false;
}
if (ignoreDefaultExcludes == null) {
if (other.ignoreDefaultExcludes != null) {
return false;
}
} else if (!ignoreDefaultExcludes.equals(other.ignoreDefaultExcludes)) {
return false;
}
if (includes == null) {
if (other.includes != null) {
return false;
}
} else if (!includes.equals(other.includes)) {
return false;
}
if (path == null) {
if (other.path != null) {
return false;
}
} else if (!path.equals(other.path)) {
return false;
}
if (skipPngCrush == null) {
if (other.skipPngCrush != null) {
return false;
}
} else if (!skipPngCrush.equals(other.skipPngCrush)) {
return false;
}
if (targetPath == null) {
if (other.targetPath != null) {
return false;
}
} else if (!targetPath.equals(other.targetPath)) {
return false;
}
return true;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Resource [targetPath=").append(targetPath)
.append(", directory=").append(directory).append(", includes=")
.append(includes).append(", excludes=").append(excludes)
.append(", flatten=").append(flatten)
.append(", ignoreDefaultExcludes=")
.append(ignoreDefaultExcludes).append(", skipPngCrush=")
.append(skipPngCrush).append(", path=").append(path)
.append("]");
return builder.toString();
}
}