Revamped the way TileSetBundler mogrifies and writes out bundles
Introduced a Writer class to hold all the options to the createBundle method and help clarify things. This also ended up being a convenient place to put the previous member variables since they affect the process in pretty much the same way. Got rid of DirectoryTileSetBundler. This had a mutated copy of a method from TileSetBundler, that appeared to be missing some bug fixes. Anyway, it shouldn't be needed anymore, callers can use a BundleWriter that is a directory. Added an option for outputting tileset bundle metadata as json. This includes some utility classes and the net.sf.json-lib dependency. This follows the same pattern as some other tools only jars so should end up being ignored by production applications.
This commit is contained in:
committed by
Michael Bayne
parent
426165b85e
commit
bce83d858e
@@ -35,10 +35,14 @@ import com.threerings.resource.ResourceBundle;
|
|||||||
*/
|
*/
|
||||||
public class BundleUtil
|
public class BundleUtil
|
||||||
{
|
{
|
||||||
/** The path to the metadata resource that we will attempt to load
|
/** The path to the binary metadata resource that jvm clients will attempt to load
|
||||||
* from our resource set. */
|
* from the bundle's resource set. */
|
||||||
public static final String METADATA_PATH = "tsbundles.dat";
|
public static final String METADATA_PATH = "tsbundles.dat";
|
||||||
|
|
||||||
|
/** The path to the text metadata resource that playn clients will attempt to load
|
||||||
|
* from the bundle's resource set. */
|
||||||
|
public static final String METADATA_JSON_PATH = "tsbundles.json";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts, but does not initialize, a serialized tileset bundle
|
* Extracts, but does not initialize, a serialized tileset bundle
|
||||||
* instance from the supplied resource bundle.
|
* instance from the supplied resource bundle.
|
||||||
|
|||||||
@@ -38,6 +38,13 @@
|
|||||||
<artifactId>playn-core</artifactId>
|
<artifactId>playn-core</artifactId>
|
||||||
<version>1.5.1</version>
|
<version>1.5.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sf.json-lib</groupId>
|
||||||
|
<artifactId>json-lib</artifactId>
|
||||||
|
<version>2.4</version>
|
||||||
|
<classifier>jdk15</classifier>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
<!-- if you add a dependency here, be sure to hack it into build.xml, yay Ant! -->
|
<!-- if you add a dependency here, be sure to hack it into build.xml, yay Ant! -->
|
||||||
|
|
||||||
<!-- test/build dependencies -->
|
<!-- test/build dependencies -->
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
// Nenya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://code.google.com/p/nenya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library 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
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
package com.threerings.media.tile.bundle.tools;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.zip.Deflater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes files to disk either in a directory in a jar. The majority of the calling code doesn't
|
||||||
|
* need to distinguish. Note that jars are not opened for appending, the class is intended to be
|
||||||
|
* used in situations where the contents can be generated entirely.
|
||||||
|
*/
|
||||||
|
public class BundleWriter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new bundle writer that will write to the given target jar file.
|
||||||
|
* @param target file to open as a jar
|
||||||
|
* @param uncompressed whether compression is off
|
||||||
|
*/
|
||||||
|
public BundleWriter (File target, final boolean uncompressed)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
_target = target;
|
||||||
|
_dir = null;
|
||||||
|
_jar = new JarImpl();
|
||||||
|
_jar.uncompressed = uncompressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new bundle writer that will write files into the given directory.
|
||||||
|
* @param targetDir directory to write files to
|
||||||
|
* @throws IOException if the directory cannot be created
|
||||||
|
*/
|
||||||
|
public BundleWriter (File targetDir)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
_target = targetDir;
|
||||||
|
_jar = null;
|
||||||
|
_dir = new DirImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a new file for writing in the jar or directory.
|
||||||
|
*/
|
||||||
|
public OutputStream startNewFile (String path)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
if (_jar != null) {
|
||||||
|
_jar.get().putNextEntry(new JarEntry(path));
|
||||||
|
return _jar.get();
|
||||||
|
} else {
|
||||||
|
_dir.closeFile();
|
||||||
|
return _dir.newFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if the target is newer than the given time. Note that if we are in directory-mode,
|
||||||
|
* false is returned.
|
||||||
|
*/
|
||||||
|
public boolean isNewerThan (long newest)
|
||||||
|
{
|
||||||
|
if (_jar != null) {
|
||||||
|
return _target.lastModified() > newest;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the writer.
|
||||||
|
*/
|
||||||
|
public void close ()
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
if (_jar != null) {
|
||||||
|
_jar.get().close();
|
||||||
|
} else {
|
||||||
|
_dir.closeFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString ()
|
||||||
|
{
|
||||||
|
return _target.toString() + (_jar == null ? "/" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the target.
|
||||||
|
*/
|
||||||
|
public boolean delete ()
|
||||||
|
{
|
||||||
|
if (_jar != null) {
|
||||||
|
return _target.delete();
|
||||||
|
}
|
||||||
|
// TODO: delete directory
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class JarImpl
|
||||||
|
{
|
||||||
|
JarOutputStream get ()
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
if (jar == null) {
|
||||||
|
FileOutputStream fout = new FileOutputStream(_target);
|
||||||
|
Manifest manifest = new Manifest();
|
||||||
|
jar = new JarOutputStream(fout, manifest);
|
||||||
|
jar.setLevel(uncompressed ? Deflater.NO_COMPRESSION : Deflater.BEST_COMPRESSION);
|
||||||
|
}
|
||||||
|
return jar;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarOutputStream jar;
|
||||||
|
boolean uncompressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class DirImpl
|
||||||
|
{
|
||||||
|
public DirImpl ()
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
_target.mkdirs();
|
||||||
|
if (!_target.isDirectory()) {
|
||||||
|
throw new IOException("Not a directory: " + _target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeFile ()
|
||||||
|
{
|
||||||
|
if (current != null) {
|
||||||
|
try {
|
||||||
|
current.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
System.err.println("Unable to close file: " + current);
|
||||||
|
}
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutputStream newFile (String path)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return current = new FileOutputStream(new File(_target, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputStream current;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final File _target;
|
||||||
|
protected final JarImpl _jar;
|
||||||
|
protected final DirImpl _dir;
|
||||||
|
}
|
||||||
-171
@@ -1,171 +0,0 @@
|
|||||||
//
|
|
||||||
// Nenya library - tools for developing networked games
|
|
||||||
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
|
||||||
// https://github.com/threerings/nenya
|
|
||||||
//
|
|
||||||
// This library is free software; you can redistribute it and/or modify it
|
|
||||||
// under the terms of the GNU Lesser General Public License as published
|
|
||||||
// by the Free Software Foundation; either version 2.1 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This library 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
|
|
||||||
// Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public
|
|
||||||
// License along with this library; if not, write to the Free Software
|
|
||||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
|
|
||||||
package com.threerings.media.tile.bundle.tools;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectOutputStream;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
import com.samskivert.io.StreamUtil;
|
|
||||||
|
|
||||||
import com.threerings.media.tile.ImageProvider;
|
|
||||||
import com.threerings.media.tile.ObjectTileSet;
|
|
||||||
import com.threerings.media.tile.TileSet;
|
|
||||||
import com.threerings.media.tile.TrimmedObjectTileSet;
|
|
||||||
import com.threerings.media.tile.bundle.BundleUtil;
|
|
||||||
import com.threerings.media.tile.bundle.TileSetBundle;
|
|
||||||
|
|
||||||
import static com.threerings.media.Log.log;
|
|
||||||
|
|
||||||
public class DirectoryTileSetBundler extends TileSetBundler
|
|
||||||
{
|
|
||||||
public DirectoryTileSetBundler (File configFile)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
super(configFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DirectoryTileSetBundler (String configPath)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
super(configPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean createBundle (
|
|
||||||
File target, TileSetBundle bundle, ImageProvider improv, String imageBase, long newestMod)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
// write all of the image files to the bundle's target path, converting the
|
|
||||||
// tilesets to trimmed tilesets in the process
|
|
||||||
Iterator<Integer> iditer = bundle.enumerateTileSetIds();
|
|
||||||
while (iditer.hasNext()) {
|
|
||||||
int tileSetId = iditer.next().intValue();
|
|
||||||
TileSet set = bundle.getTileSet(tileSetId);
|
|
||||||
String imagePath = set.getImagePath();
|
|
||||||
|
|
||||||
// sanity checks
|
|
||||||
if (imagePath == null) {
|
|
||||||
log.warning("Tileset contains no image path " +
|
|
||||||
"[set=" + set + "]. It ain't gonna work.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this is an object tileset, trim it
|
|
||||||
if (set instanceof ObjectTileSet) {
|
|
||||||
// set the tileset up with an image provider; we
|
|
||||||
// need to do this so that we can trim it!
|
|
||||||
set.setImageProvider(improv);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// create a trimmed object tileset, which will
|
|
||||||
// write the trimmed tileset image to the destination output stream
|
|
||||||
File outFile = new File(target, imagePath);
|
|
||||||
FileOutputStream fout = null;
|
|
||||||
|
|
||||||
if (outFile.lastModified() > newestMod) {
|
|
||||||
// Our file's newer than the newest bundle mod - up to date.
|
|
||||||
// So don't actually do anything
|
|
||||||
|
|
||||||
// TODO: Ideally, we'd like to skip re-trimming altogether, since that's
|
|
||||||
// expensive, but for the moment, we're at least doing half as much by
|
|
||||||
// not writing out the trimmed image.
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// It's changed, so let's open the file & do all that jazz
|
|
||||||
outFile.getParentFile().mkdirs();
|
|
||||||
fout = new FileOutputStream(outFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
TrimmedObjectTileSet tset =
|
|
||||||
TrimmedObjectTileSet.trimObjectTileSet((ObjectTileSet)set, fout, "png");
|
|
||||||
tset.setImagePath(imagePath);
|
|
||||||
// replace the original set with the trimmed tileset in the tileset bundle
|
|
||||||
bundle.addTileSet(tileSetId, tset);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
|
|
||||||
String msg = "Error adding tileset to bundle " + imagePath +
|
|
||||||
", " + set.getName() + ": " + e;
|
|
||||||
throw (IOException) new IOException(msg).initCause(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// read the image file and write it to the proper place
|
|
||||||
File ifile = new File(imageBase, imagePath);
|
|
||||||
if (ifile.lastModified() > newestMod) {
|
|
||||||
// Our file's newer than the newest bundle mod - up to date.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// We read the image to ensure it is a valid image.
|
|
||||||
ImageIO.read(ifile);
|
|
||||||
|
|
||||||
File outFile = new File(target, imagePath);
|
|
||||||
if (outFile.lastModified() > newestMod) {
|
|
||||||
// Our file's newer than the newest bundle mod - up to date.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
outFile.getParentFile().mkdirs();
|
|
||||||
FileOutputStream fout = new FileOutputStream(outFile);
|
|
||||||
FileInputStream imgin = new FileInputStream(ifile);
|
|
||||||
StreamUtil.copy(imgin, fout);
|
|
||||||
} catch (Exception e) {
|
|
||||||
String msg = "Failure bundling image " + ifile + ": " + e;
|
|
||||||
throw (IOException) new IOException(msg).initCause(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now write a serialized representation of the tileset bundle
|
|
||||||
// object to the bundle jar file
|
|
||||||
File outFile = new File(target, BundleUtil.METADATA_PATH);
|
|
||||||
|
|
||||||
outFile.getParentFile().mkdirs();
|
|
||||||
FileOutputStream fout = new FileOutputStream(outFile);
|
|
||||||
ObjectOutputStream oout = new ObjectOutputStream(fout);
|
|
||||||
oout.writeObject(bundle);
|
|
||||||
oout.flush();
|
|
||||||
fout.close();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
String errmsg = "Failed to create bundle " + target + ": " + e;
|
|
||||||
throw (IOException) new IOException(errmsg).initCause(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean skipIfTargetNewer ()
|
|
||||||
{
|
|
||||||
// We have to check modification later on a file-by-file basis, so cannot skip.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-8
@@ -46,17 +46,11 @@ public class DirectoryTileSetBundlerTask extends TileSetBundlerTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TileSetBundler createBundler ()
|
protected BundleWriter createWriter (File fromDir, String path)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
|
||||||
return new DirectoryTileSetBundler(_config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getTargetPath (File fromDir, String path)
|
|
||||||
{
|
{
|
||||||
File xmlFile = new File(path.replace(fromDir.getPath(), _deployDir.getPath()));
|
File xmlFile = new File(path.replace(fromDir.getPath(), _deployDir.getPath()));
|
||||||
return xmlFile.getParent();
|
return new BundleWriter(xmlFile.getParentFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The directory in which we want to place our tile set files for deployment. */
|
/** The directory in which we want to place our tile set files for deployment. */
|
||||||
|
|||||||
@@ -21,21 +21,21 @@ package com.threerings.media.tile.bundle.tools;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
import java.util.jar.JarOutputStream;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
import java.util.zip.Deflater;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectOutputStream;
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import net.sf.json.JSONArray;
|
||||||
|
import net.sf.json.JSONObject;
|
||||||
|
|
||||||
import org.apache.commons.digester.Digester;
|
import org.apache.commons.digester.Digester;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
@@ -45,6 +45,8 @@ import com.samskivert.util.HashIntMap;
|
|||||||
|
|
||||||
import com.threerings.resource.FastImageIO;
|
import com.threerings.resource.FastImageIO;
|
||||||
|
|
||||||
|
import com.threerings.tools.JSONConversion;
|
||||||
|
|
||||||
import com.threerings.media.tile.ImageProvider;
|
import com.threerings.media.tile.ImageProvider;
|
||||||
import com.threerings.media.tile.ObjectTileSet;
|
import com.threerings.media.tile.ObjectTileSet;
|
||||||
import com.threerings.media.tile.SimpleCachingImageProvider;
|
import com.threerings.media.tile.SimpleCachingImageProvider;
|
||||||
@@ -112,6 +114,90 @@ import static com.threerings.media.Log.log;
|
|||||||
*/
|
*/
|
||||||
public class TileSetBundler
|
public class TileSetBundler
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Wraps up the configuration options for writing a tile set bundle to disk and provides a
|
||||||
|
* method to perform the write.
|
||||||
|
*/
|
||||||
|
public static class Writer
|
||||||
|
{
|
||||||
|
/** The target jar file or directory. */
|
||||||
|
public final BundleWriter bwriter;
|
||||||
|
|
||||||
|
/** The bundle to write. */
|
||||||
|
public final TileSetBundle bundle;
|
||||||
|
|
||||||
|
/** Loads images for the bundle. */
|
||||||
|
public final ImageProvider improv;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new writer.
|
||||||
|
*
|
||||||
|
* @param bwriter the tileset bundle file or directory that will be created.
|
||||||
|
* @param bundle contains the tilesets we'd like to save out to the bundle.
|
||||||
|
* @param improv the image provider.
|
||||||
|
*/
|
||||||
|
public Writer (BundleWriter bwriter, TileSetBundle bundle, ImageProvider improv)
|
||||||
|
{
|
||||||
|
this.bundle = bundle;
|
||||||
|
this.bwriter = bwriter;
|
||||||
|
this.improv = improv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether or not we trim the tileset's image in the process of writing out the
|
||||||
|
* new bundle. Trimming will mutate tilesets directly, so beware.
|
||||||
|
*/
|
||||||
|
public Writer trimImages (boolean trim)
|
||||||
|
{
|
||||||
|
this.trim = trim;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether or not we write out raw images.
|
||||||
|
*/
|
||||||
|
public Writer useRawImages (boolean raw)
|
||||||
|
{
|
||||||
|
this.raw = raw;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the image base.
|
||||||
|
* TODO: learn what this does and improve comment. should it just be a special improv?
|
||||||
|
*/
|
||||||
|
public Writer imageBase (String imageBase)
|
||||||
|
{
|
||||||
|
this.imageBase = imageBase;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether we write out a json file containing the meta data.
|
||||||
|
* @param json if non-null, used to write out the bundle contents to tsbundles.json text
|
||||||
|
* file. Otherwise, the default binary obbject output is used.
|
||||||
|
*/
|
||||||
|
public Writer useJson (JSONConversion.Config json)
|
||||||
|
{
|
||||||
|
this.json = json;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using the configured options, creates the new target bundle.
|
||||||
|
*/
|
||||||
|
public void create ()
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
TileSetBundler.createBundle(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean trim = true;
|
||||||
|
boolean raw = true;
|
||||||
|
String imageBase;
|
||||||
|
JSONConversion.Config json;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tileset bundler with the specified path to a bundler
|
* Constructs a tileset bundler with the specified path to a bundler
|
||||||
* configuration file. The configuration file will be loaded and used
|
* configuration file. The configuration file will be loaded and used
|
||||||
@@ -130,19 +216,6 @@ public class TileSetBundler
|
|||||||
public TileSetBundler (File configFile)
|
public TileSetBundler (File configFile)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
this(configFile, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a tileset bundler with the specified bundler config
|
|
||||||
* file and whether to keep pngs as-is or if not, re-encode them.
|
|
||||||
*/
|
|
||||||
public TileSetBundler (File configFile, boolean keepRawPngs, boolean uncompressed)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
_keepRawPngs = keepRawPngs;
|
|
||||||
_uncompressed = uncompressed;
|
|
||||||
|
|
||||||
// we parse our configuration with a digester
|
// we parse our configuration with a digester
|
||||||
Digester digester = new Digester();
|
Digester digester = new Digester();
|
||||||
|
|
||||||
@@ -198,46 +271,24 @@ public class TileSetBundler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a tileset bundle at the location specified by the
|
* Prepares to create a tileset bundle at the location specified by the
|
||||||
* <code>targetPath</code> parameter, based on the description
|
* <code>target</code> parameter, based on the description
|
||||||
* provided via the <code>bundleDesc</code> parameter.
|
* provided via the <code>bundleDesc</code> parameter.
|
||||||
*
|
*
|
||||||
* @param idBroker the tileset id broker that will be used to map
|
* @param idBroker the tileset id broker that will be used to map
|
||||||
* tileset names to tileset ids.
|
* tileset names to tileset ids.
|
||||||
* @param bundleDesc a file object pointing to the bundle description
|
* @param bundleDesc a file object pointing to the bundle description
|
||||||
* file.
|
* file.
|
||||||
* @param targetPath the path of the tileset bundle file that will be
|
* @param target the tileset bundle file or directory that will be created.
|
||||||
* created.
|
*
|
||||||
|
* @return a writer object that can be used to configure and create the target
|
||||||
|
* bundle, or null if the target is up to date with respect to all source files
|
||||||
*
|
*
|
||||||
* @exception IOException thrown if an error occurs reading, writing
|
* @exception IOException thrown if an error occurs reading, writing
|
||||||
* or processing anything.
|
* or processing anything.
|
||||||
*/
|
*/
|
||||||
public void createBundle (
|
public Writer process (
|
||||||
TileSetIDBroker idBroker, File bundleDesc, String targetPath)
|
TileSetIDBroker idBroker, final File bundleDesc, BundleWriter target)
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
createBundle(idBroker, bundleDesc, new File(targetPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a tileset bundle at the location specified by the
|
|
||||||
* <code>targetPath</code> parameter, based on the description
|
|
||||||
* provided via the <code>bundleDesc</code> parameter.
|
|
||||||
*
|
|
||||||
* @param idBroker the tileset id broker that will be used to map
|
|
||||||
* tileset names to tileset ids.
|
|
||||||
* @param bundleDesc a file object pointing to the bundle description
|
|
||||||
* file.
|
|
||||||
* @param target the tileset bundle file that will be created.
|
|
||||||
*
|
|
||||||
* @return true if the bundle was rebuilt, false if it was not because
|
|
||||||
* the bundle file was newer than all involved source files.
|
|
||||||
*
|
|
||||||
* @exception IOException thrown if an error occurs reading, writing
|
|
||||||
* or processing anything.
|
|
||||||
*/
|
|
||||||
public boolean createBundle (
|
|
||||||
TileSetIDBroker idBroker, final File bundleDesc, File target)
|
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
// stick an array list on the top of the stack into which we will
|
// stick an array list on the top of the stack into which we will
|
||||||
@@ -319,8 +370,8 @@ public class TileSetBundler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// see if our newest file is newer than the tileset bundle
|
// see if our newest file is newer than the tileset bundle
|
||||||
if (skipIfTargetNewer() && newest < target.lastModified()) {
|
if (target.isNewerThan(newest)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create an image provider for loading our tileset images
|
// create an image provider for loading our tileset images
|
||||||
@@ -332,59 +383,26 @@ public class TileSetBundler
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return createBundle(target, bundle, improv, bundleDesc.getParent(), newest);
|
return new Writer(target, bundle, improv).imageBase(bundleDesc.getParent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finish the creation of a tileset bundle jar file.
|
* Create the tileset bundle on disk using previously configured options.
|
||||||
*
|
|
||||||
* @param target the tileset bundle file that will be created.
|
|
||||||
* @param bundle contains the tilesets we'd like to save out to the bundle.
|
|
||||||
* @param improv the image provider.
|
|
||||||
* @param imageBase the base directory for getting images for non
|
|
||||||
* @param newestMod the most recent modification to any part of the bundle. By default we
|
|
||||||
* ignore this since we normally duck out if we're up to date.
|
|
||||||
* ObjectTileSet tilesets.
|
|
||||||
*/
|
*/
|
||||||
public boolean createBundle (
|
protected static void createBundle (Writer target)
|
||||||
File target, TileSetBundle bundle, ImageProvider improv, String imageBase, long newestMod)
|
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
return createBundleJar(target, bundle, improv, imageBase, _keepRawPngs, _uncompressed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a tileset bundle jar file.
|
|
||||||
*
|
|
||||||
* @param target the tileset bundle file that will be created.
|
|
||||||
* @param bundle contains the tilesets we'd like to save out to the bundle.
|
|
||||||
* @param improv the image provider.
|
|
||||||
* @param imageBase the base directory for getting images for non-ObjectTileSet tilesets.
|
|
||||||
* @param keepOriginalPngs bundle up the original PNGs as PNGs instead of converting to the
|
|
||||||
* FastImageIO raw format
|
|
||||||
*/
|
|
||||||
public static boolean createBundleJar (
|
|
||||||
File target, TileSetBundle bundle, ImageProvider improv, String imageBase,
|
|
||||||
boolean keepOriginalPngs, boolean uncompressed)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
// now we have to create the actual bundle file
|
|
||||||
FileOutputStream fout = new FileOutputStream(target);
|
|
||||||
Manifest manifest = new Manifest();
|
|
||||||
JarOutputStream jar = new JarOutputStream(fout, manifest);
|
|
||||||
jar.setLevel(uncompressed ? Deflater.NO_COMPRESSION : Deflater.BEST_COMPRESSION);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// write all of the image files to the bundle, converting the
|
// write all of the image files to the bundle, converting the
|
||||||
// tilesets to trimmed tilesets in the process
|
// tilesets to trimmed tilesets in the process
|
||||||
Iterator<Integer> iditer = bundle.enumerateTileSetIds();
|
Iterator<Integer> iditer = target.bundle.enumerateTileSetIds();
|
||||||
|
|
||||||
// Store off the updated TileSets in a separate Map so we can wait to change the
|
// Store off the updated TileSets in a separate Map so we can wait to change the
|
||||||
// bundle till we're done iterating.
|
// bundle till we're done iterating.
|
||||||
HashIntMap<TileSet> toUpdate = new HashIntMap<TileSet>();
|
HashIntMap<TileSet> toUpdate = new HashIntMap<TileSet>();
|
||||||
while (iditer.hasNext()) {
|
while (iditer.hasNext()) {
|
||||||
int tileSetId = iditer.next().intValue();
|
int tileSetId = iditer.next().intValue();
|
||||||
TileSet set = bundle.getTileSet(tileSetId);
|
TileSet set = target.bundle.getTileSet(tileSetId);
|
||||||
String imagePath = set.getImagePath();
|
String imagePath = set.getImagePath();
|
||||||
|
|
||||||
// sanity checks
|
// sanity checks
|
||||||
@@ -395,22 +413,23 @@ public class TileSetBundler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if this is an object tileset, trim it
|
// if this is an object tileset, trim it
|
||||||
if (!keepOriginalPngs && (set instanceof ObjectTileSet)) {
|
if (target.trim && (set instanceof ObjectTileSet)) {
|
||||||
// set the tileset up with an image provider; we
|
// set the tileset up with an image provider; we
|
||||||
// need to do this so that we can trim it!
|
// need to do this so that we can trim it!
|
||||||
set.setImageProvider(improv);
|
set.setImageProvider(target.improv);
|
||||||
|
|
||||||
// we're going to trim it, so adjust the path
|
// add .raw if requested
|
||||||
|
if (target.raw) {
|
||||||
imagePath = adjustImagePath(imagePath);
|
imagePath = adjustImagePath(imagePath);
|
||||||
jar.putNextEntry(new JarEntry(imagePath));
|
}
|
||||||
|
OutputStream dest = target.bwriter.startNewFile(imagePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// create a trimmed object tileset, which will
|
// create a trimmed object tileset, which will
|
||||||
// write the trimmed tileset image to the jar
|
// write the trimmed tileset image to the target file
|
||||||
// output stream
|
|
||||||
TrimmedObjectTileSet tset =
|
TrimmedObjectTileSet tset =
|
||||||
TrimmedObjectTileSet.trimObjectTileSet(
|
TrimmedObjectTileSet.trimObjectTileSet((ObjectTileSet)set, dest,
|
||||||
(ObjectTileSet)set, jar);
|
target.raw ? FastImageIO.FILE_SUFFIX : "png");
|
||||||
tset.setImagePath(imagePath);
|
tset.setImagePath(imagePath);
|
||||||
// replace the original set with the trimmed
|
// replace the original set with the trimmed
|
||||||
// tileset in the tileset bundle
|
// tileset in the tileset bundle
|
||||||
@@ -427,18 +446,21 @@ public class TileSetBundler
|
|||||||
} else {
|
} else {
|
||||||
// read the image file and convert it to our custom
|
// read the image file and convert it to our custom
|
||||||
// format in the bundle
|
// format in the bundle
|
||||||
File ifile = new File(imageBase, imagePath);
|
File ifile = new File(target.imageBase, imagePath);
|
||||||
try {
|
try {
|
||||||
BufferedImage image = ImageIO.read(ifile);
|
BufferedImage image = ImageIO.read(ifile);
|
||||||
if (!keepOriginalPngs && FastImageIO.canWrite(image)) {
|
if (target.raw && FastImageIO.canWrite(image)) {
|
||||||
imagePath = adjustImagePath(imagePath);
|
imagePath = adjustImagePath(imagePath);
|
||||||
jar.putNextEntry(new JarEntry(imagePath));
|
// NOTE: DirectoryTileSetBundler used to check the modification time
|
||||||
|
// of the target image here. There may be something to it, but it
|
||||||
|
// looked unnecessary
|
||||||
|
OutputStream dest = target.bwriter.startNewFile(imagePath);
|
||||||
set.setImagePath(imagePath);
|
set.setImagePath(imagePath);
|
||||||
FastImageIO.write(image, jar);
|
FastImageIO.write(image, dest);
|
||||||
} else {
|
} else {
|
||||||
jar.putNextEntry(new JarEntry(imagePath));
|
OutputStream dest = target.bwriter.startNewFile(imagePath);
|
||||||
FileInputStream imgin = new FileInputStream(ifile);
|
FileInputStream imgin = new FileInputStream(ifile);
|
||||||
StreamUtil.copy(imgin, jar);
|
StreamUtil.copy(imgin, dest);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String msg = "Failure bundling image " + ifile +
|
String msg = "Failure bundling image " + ifile +
|
||||||
@@ -447,25 +469,39 @@ public class TileSetBundler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bundle.putAll(toUpdate);
|
target.bundle.putAll(toUpdate);
|
||||||
|
|
||||||
// now write a serialized representation of the tileset bundle
|
// now write a serialized representation of the tileset bundle
|
||||||
|
if (target.json != null) {
|
||||||
|
JSONArray array = new JSONArray();
|
||||||
|
for (Iterator<Integer> tileSetId = target.bundle.enumerateTileSetIds();
|
||||||
|
tileSetId.hasNext(); ) {
|
||||||
|
JSONObject tset = new JSONObject();
|
||||||
|
int id = tileSetId.next();
|
||||||
|
tset.put("id", id);
|
||||||
|
tset.put("set", target.json.convert(target.bundle.get(id)));
|
||||||
|
array.add(tset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now write a serialized representation of the tileset bundle
|
||||||
|
OutputStream fout = target.bwriter.startNewFile(BundleUtil.METADATA_JSON_PATH);
|
||||||
|
fout.write(array.toString().getBytes());
|
||||||
|
fout.close();
|
||||||
|
} else {
|
||||||
// object to the bundle jar file
|
// object to the bundle jar file
|
||||||
JarEntry entry = new JarEntry(BundleUtil.METADATA_PATH);
|
ObjectOutputStream oout = new ObjectOutputStream(
|
||||||
jar.putNextEntry(entry);
|
target.bwriter.startNewFile(BundleUtil.METADATA_PATH));
|
||||||
ObjectOutputStream oout = new ObjectOutputStream(jar);
|
oout.writeObject(target.bundle);
|
||||||
oout.writeObject(bundle);
|
|
||||||
oout.flush();
|
oout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
// finally close up the jar file and call ourself done
|
// finally close up the jar file and call ourself done
|
||||||
jar.close();
|
target.bwriter.close();
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// remove the incomplete jar file and rethrow the exception
|
// remove the incomplete jar file and rethrow the exception
|
||||||
jar.close();
|
target.bwriter.close();
|
||||||
if (!target.delete()) {
|
if (!target.bwriter.delete()) {
|
||||||
log.warning("Failed to close botched bundle '" + target + "'.");
|
log.warning("Failed to close botched bundle '" + target + "'.");
|
||||||
}
|
}
|
||||||
String errmsg = "Failed to create bundle " + target + ": " + e;
|
String errmsg = "Failed to create bundle " + target + ": " + e;
|
||||||
@@ -473,14 +509,6 @@ public class TileSetBundler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether we should skip updating the bundle if the target is newer than any component.
|
|
||||||
*/
|
|
||||||
protected boolean skipIfTargetNewer ()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Replaces the image suffix with <code>.raw</code>. */
|
/** Replaces the image suffix with <code>.raw</code>. */
|
||||||
protected static String adjustImagePath (String imagePath)
|
protected static String adjustImagePath (String imagePath)
|
||||||
{
|
{
|
||||||
@@ -511,10 +539,4 @@ public class TileSetBundler
|
|||||||
|
|
||||||
/** The digester we use to parse bundle descriptions. */
|
/** The digester we use to parse bundle descriptions. */
|
||||||
protected Digester _digester;
|
protected Digester _digester;
|
||||||
|
|
||||||
/** Whether we should keep pngs as-is rather than re-encoding. */
|
|
||||||
protected boolean _keepRawPngs;
|
|
||||||
|
|
||||||
/** Normally we compress the jar, but if we want to leave them uncompressed, we set this. */
|
|
||||||
protected boolean _uncompressed;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-15
@@ -97,7 +97,7 @@ public class TileSetBundlerTask extends Task
|
|||||||
File cfile = null;
|
File cfile = null;
|
||||||
try {
|
try {
|
||||||
// create a tileset bundler
|
// create a tileset bundler
|
||||||
TileSetBundler bundler = createBundler();
|
TileSetBundler bundler = new TileSetBundler(_config);
|
||||||
|
|
||||||
// create our tileset id broker
|
// create our tileset id broker
|
||||||
MapFileTileSetIDBroker broker =
|
MapFileTileSetIDBroker broker =
|
||||||
@@ -123,16 +123,18 @@ public class TileSetBundlerTask extends Task
|
|||||||
"Config file should end with .xml.");
|
"Config file should end with .xml.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String bpath = getTargetPath(fromDir, cpath);
|
BundleWriter bwriter = createWriter(fromDir, cpath);
|
||||||
File bfile = new File(bpath);
|
|
||||||
|
|
||||||
// create the bundle
|
// create the bundle
|
||||||
if (bundler.createBundle(broker, cfile, bfile)) {
|
TileSetBundler.Writer writer = bundler.process(broker, cfile, bwriter);
|
||||||
|
if (writer != null) {
|
||||||
|
writer.useRawImages(!_keepRawPngs); // something is lost in translation here
|
||||||
|
writer.create();
|
||||||
System.out.println(
|
System.out.println(
|
||||||
"Created bundle from '" + cpath + "'...");
|
"Created bundle from '" + cpath + "'...");
|
||||||
} else {
|
} else {
|
||||||
System.out.println(
|
System.out.println(
|
||||||
"Tileset bundle up to date '" + bpath + "'.");
|
"Tileset bundle up to date '" + bwriter + "'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,18 +152,11 @@ public class TileSetBundlerTask extends Task
|
|||||||
/**
|
/**
|
||||||
* Create the bundler to use during creation.
|
* Create the bundler to use during creation.
|
||||||
*/
|
*/
|
||||||
protected TileSetBundler createBundler ()
|
protected BundleWriter createWriter (File fromDir, String path)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
return new TileSetBundler(_config, _keepRawPngs, _uncompressed);
|
return new BundleWriter(
|
||||||
}
|
new File(path.substring(0, path.length()-4) + ".jar"), _uncompressed);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the target path in which our bundler will write the tile set.
|
|
||||||
*/
|
|
||||||
protected String getTargetPath (File fromDir, String path)
|
|
||||||
{
|
|
||||||
return path.substring(0, path.length()-4) + ".jar";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void ensureSet (Object value, String errmsg)
|
protected void ensureSet (Object value, String errmsg)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Nenya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://code.google.com/p/nenya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library 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
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
package com.threerings.tools;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Point;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
|
||||||
|
import net.sf.json.JSONArray;
|
||||||
|
|
||||||
|
import com.threerings.tools.JSONConversion.Config;
|
||||||
|
import com.threerings.tools.JSONConversion.Converter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines some common formats for converting nenya's uses of java.awt geometric types to json.
|
||||||
|
*/
|
||||||
|
public class AWTConversions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds all our conversion to the given config and returns it for chaining.
|
||||||
|
*/
|
||||||
|
public static Config addAll (Config config)
|
||||||
|
{
|
||||||
|
return config.
|
||||||
|
addClassConverter(Rectangle.class, RECTANGLE).
|
||||||
|
addClassConverter(Point.class, POINT).
|
||||||
|
addClassConverter(Dimension.class, DIMENSION).
|
||||||
|
addClassConverter(Color.class, COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts instances of color to a standard RGB byte triplet.
|
||||||
|
*/
|
||||||
|
public static final Converter<Color> COLOR = new Converter<Color>() {
|
||||||
|
public Object convert (Color c, Config cfg) {
|
||||||
|
return new Integer((c.getRed() << 16) | (c.getGreen()<<8) | c.getBlue());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a rectangle into an json array with entries x, y, width, height.
|
||||||
|
*/
|
||||||
|
public static final Converter<Rectangle> RECTANGLE = new Converter<Rectangle>() {
|
||||||
|
public Object convert (Rectangle r, Config cfg) {
|
||||||
|
JSONArray out = new JSONArray();
|
||||||
|
out.add(r.x);
|
||||||
|
out.add(r.y);
|
||||||
|
out.add(r.width);
|
||||||
|
out.add(r.height);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a point into a json array with entries x, y.
|
||||||
|
*/
|
||||||
|
public static final Converter<Point> POINT = new Converter<Point>() {
|
||||||
|
public Object convert (Point p, Config cfg) {
|
||||||
|
JSONArray out = new JSONArray();
|
||||||
|
out.add(p.x);
|
||||||
|
out.add(p.y);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a dimension into an array with entries width, height.
|
||||||
|
*/
|
||||||
|
public static final Converter<Dimension> DIMENSION = new Converter<Dimension>() {
|
||||||
|
public Object convert (Dimension d, Config cfg) {
|
||||||
|
JSONArray out = new JSONArray();
|
||||||
|
out.add(d.width);
|
||||||
|
out.add(d.height);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
// Nenya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://code.google.com/p/nenya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library 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
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
package com.threerings.tools;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.security.AccessController;
|
||||||
|
import java.security.PrivilegedActionException;
|
||||||
|
import java.security.PrivilegedExceptionAction;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
|
import com.samskivert.util.ClassUtil;
|
||||||
|
import com.samskivert.util.StringUtil;
|
||||||
|
|
||||||
|
import net.sf.json.JSON;
|
||||||
|
import net.sf.json.JSONArray;
|
||||||
|
import net.sf.json.JSONObject;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for converting instances of java objects to json. This is basically automatic for
|
||||||
|
* simple classes, but field enumeration and overall conversion can be customized. <br/><br/>
|
||||||
|
*
|
||||||
|
* Example: <pre>
|
||||||
|
* Config cfg = new Config();
|
||||||
|
* System.out.println(cfg.convert(new Pojo()));
|
||||||
|
* </pre>
|
||||||
|
* Converting a type universally: <pre>
|
||||||
|
Converter<Color> colorConv = new Converter<Color>() {
|
||||||
|
public Object convert (Color c, Config cfg) {
|
||||||
|
return new Integer((c.getRed() << 16) | (c.getGreen()<<8) | c.getBlue());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg.addClassConverter(Color.class, colorConv);
|
||||||
|
* </pre>
|
||||||
|
* Customizing a specific field of a class:<pre>
|
||||||
|
FieldAccessor<SomeImpl, Collection<?>> entryConv =
|
||||||
|
new FieldAccessor<SomeImpl, Collection<?>>() {
|
||||||
|
public Collection<SomeValue> get (SomeImpl impl) {
|
||||||
|
return impl.getValuesAsCollection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cfg.addFieldConfig(SomeImpl.class, new FieldConfig<SomeImpl>().
|
||||||
|
addExclusions("privateStuff").
|
||||||
|
addFieldConverter("valueMap", new ConvertCollectionFieldToArray<SomeImpl>(entryConv)));
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class JSONConversion
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Controls verbose logging for the class.
|
||||||
|
*/
|
||||||
|
public static boolean vlog = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the given arguments separated by spaces, if {@link #vlog} is set.
|
||||||
|
*/
|
||||||
|
public static void vlog (Object...args)
|
||||||
|
{
|
||||||
|
if (vlog) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("JSONConversion: ");
|
||||||
|
StringUtil.toString(sb, args, "", "", " ");
|
||||||
|
System.out.println(sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts T to a suitable parameter for {@link JSONArray#add(Object)},
|
||||||
|
* {@link JSONObject#element(String, Object)}, etc.
|
||||||
|
*/
|
||||||
|
public static interface Converter<T>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns an instance of {@code T} in json form. The cfg may be used to convert aggregated
|
||||||
|
* objects.
|
||||||
|
*/
|
||||||
|
Object convert (T obj, Config cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugs into some common cases for automating the conversion of special fields. For example
|
||||||
|
* a map that is reconstructed from the values on deserialization.
|
||||||
|
*/
|
||||||
|
public static interface FieldAccessor<T, F>
|
||||||
|
{
|
||||||
|
F get (T obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the field layout for serializing instances of {@code T}.
|
||||||
|
*/
|
||||||
|
public static class FieldConfig<T>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Fields that should not be serialized.
|
||||||
|
*/
|
||||||
|
public List<String> excludeFields = Lists.newArrayList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields with custom conversions.
|
||||||
|
*/
|
||||||
|
public Map<String, Converter<?>> customFields = Maps.newHashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds some excluded fields and returns this for chaining.
|
||||||
|
*/
|
||||||
|
public FieldConfig<T> addExclusions (String ...names)
|
||||||
|
{
|
||||||
|
excludeFields.addAll(Arrays.asList(names));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a custom converter for a field and returns this for chaining.
|
||||||
|
*/
|
||||||
|
public FieldConfig<T> addFieldConverter (String name, Converter<T> field)
|
||||||
|
{
|
||||||
|
customFields.put(name, (Converter<?>)field);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if a field ought to be excluded, based on current configuration.
|
||||||
|
*/
|
||||||
|
public boolean excludeField (Field field)
|
||||||
|
{
|
||||||
|
int mask = Modifier.FINAL | Modifier.TRANSIENT | Modifier.STATIC;
|
||||||
|
return (field.getModifiers() & mask) != 0 ||
|
||||||
|
excludeFields.contains(field.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the conversion of java objects to json. Works mostly by reflection, with
|
||||||
|
* customizability.
|
||||||
|
*/
|
||||||
|
public static class Config
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Overrides the field treatment for encountered instances.
|
||||||
|
*/
|
||||||
|
public Map<Class<?>, FieldConfig<?>> classes = Maps.newHashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the conversion process for encountered instances.
|
||||||
|
*/
|
||||||
|
public Map<Class<?>, Converter<?>> converters = Maps.newHashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the way fields are serialized for instances of {@code T} and returns this
|
||||||
|
* for chaining.
|
||||||
|
*/
|
||||||
|
public <T> Config addFieldConfig (Class<T> clazz, FieldConfig<T> config)
|
||||||
|
{
|
||||||
|
classes.put(clazz, config);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the way instances of a given type are serialized and returns this for chaining.
|
||||||
|
*/
|
||||||
|
public <T> Config addClassConverter (Class<T> clazz, Converter<T> converter)
|
||||||
|
{
|
||||||
|
converters.put(clazz, converter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the field access for a class. This also makes a fairly lame attempt to
|
||||||
|
* ensure the most derived entry is used.
|
||||||
|
* TODO: spruce up class sort
|
||||||
|
*/
|
||||||
|
public FieldConfig<?> findConfig (Class<?> clazz)
|
||||||
|
{
|
||||||
|
Class<?> found = null;
|
||||||
|
for (Class<?> entry : classes.keySet()) {
|
||||||
|
if (entry.isAssignableFrom(clazz)) {
|
||||||
|
if (found == null || found.isAssignableFrom(entry)) {
|
||||||
|
found = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes.get(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if a class is primitive for our purposes.
|
||||||
|
*/
|
||||||
|
public boolean isPrimitive (Class<?> cl)
|
||||||
|
{
|
||||||
|
return cl.isPrimitive() ||
|
||||||
|
Number.class.isAssignableFrom(cl) ||
|
||||||
|
String.class.isAssignableFrom(cl) ||
|
||||||
|
Boolean.class.isAssignableFrom(cl) ||
|
||||||
|
(cl.isArray() && isPrimitive(cl.getComponentType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given object to json.
|
||||||
|
* @return either a {@link JSON} instance of an object suitable for insertion to
|
||||||
|
* {@link JSONObject#element(String, Object)}, etc.
|
||||||
|
*/
|
||||||
|
public Object convert (final Object obj)
|
||||||
|
{
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
vlog("Converting object", obj, "of", obj.getClass());
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Converter<Object> converter = (Converter<Object>)converters.get(obj.getClass());
|
||||||
|
if (converter != null) {
|
||||||
|
vlog("..using converter:", converter);
|
||||||
|
return converter.convert(obj, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrimitive(obj.getClass())) {
|
||||||
|
vlog("..returning primitive");
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.getClass().isArray()) {
|
||||||
|
vlog("..aggregating array");
|
||||||
|
JSONArray array = new JSONArray();
|
||||||
|
Object[] arr = (Object[])obj;
|
||||||
|
for (int ii = 0; ii < arr.length; ++ii) {
|
||||||
|
array.add(convert(arr[ii]));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (List.class.isAssignableFrom(obj.getClass())) {
|
||||||
|
vlog("..aggregating list");
|
||||||
|
JSONArray array = new JSONArray();
|
||||||
|
List<?> arr = (List<?>)obj;
|
||||||
|
for (int ii = 0; ii < arr.size(); ++ii) {
|
||||||
|
array.add(convert(arr.get(ii)));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add more collection types as needed
|
||||||
|
|
||||||
|
try {
|
||||||
|
return AccessController.doPrivileged(new PrivilegedExceptionAction<JSONObject>() {
|
||||||
|
public JSONObject run ()
|
||||||
|
throws Exception
|
||||||
|
{
|
||||||
|
vlog("..aggregating fields");
|
||||||
|
return convertFields(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (PrivilegedActionException ex) {
|
||||||
|
throw new RuntimeException("Failed to make object", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object to json using it's declared fields. This can be called on an
|
||||||
|
* object known to be of an appropriate composition, otherwise use {@link #convert(Object)}.
|
||||||
|
*/
|
||||||
|
public JSONObject convertFields (Object obj)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
FieldConfig<?> config = findConfig(obj.getClass());
|
||||||
|
List<Field> fields = cachedFields.get(obj.getClass());
|
||||||
|
if (fields == null) {
|
||||||
|
cachedFields.put(obj.getClass(), fields = Lists.newArrayList());
|
||||||
|
ClassUtil.getFields(obj.getClass(), fields);
|
||||||
|
vlog("Got fields for class", obj.getClass(), "fields", fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject jobj = new JSONObject();
|
||||||
|
for (Field field : fields) {
|
||||||
|
if (config != null && config.excludeField(field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Converter<Object> fieldConverter = config != null ?
|
||||||
|
(Converter<Object>)config.customFields.get(field.getName()) : null;
|
||||||
|
String fieldName = getFieldName(field);
|
||||||
|
if (fieldConverter != null) {
|
||||||
|
vlog("....converting field", field, "with name", fieldName,
|
||||||
|
"using custom converter", fieldConverter);
|
||||||
|
jobj.element(fieldName, fieldConverter.convert(obj, this));
|
||||||
|
} else {
|
||||||
|
vlog("....converting field", field, "with name", fieldName);
|
||||||
|
jobj.element(fieldName, convert(field.get(obj)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jobj;
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("Failed to make object", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the field from an object of a given name, using a doPrivileged call so that
|
||||||
|
* protected members can be accessed.
|
||||||
|
*/
|
||||||
|
public Object getFieldValue (final Object obj, final String fieldName)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
|
||||||
|
public Object run ()
|
||||||
|
throws Exception
|
||||||
|
{
|
||||||
|
List<Field> fields = cachedFields.get(obj.getClass());
|
||||||
|
if (fields == null) {
|
||||||
|
cachedFields.put(obj.getClass(), fields = Lists.newArrayList());
|
||||||
|
ClassUtil.getFields(obj.getClass(), fields);
|
||||||
|
}
|
||||||
|
for (Field field : fields) {
|
||||||
|
if (field.getName().equals(fieldName)) {
|
||||||
|
return field.get(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (PrivilegedActionException ex) {
|
||||||
|
throw new RuntimeException("Failed to make object", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to call {@link #convert(Object)} each item in an iterable and return
|
||||||
|
* an array of the results.
|
||||||
|
*/
|
||||||
|
public <T> JSONArray toArray (Iterable<T> objects)
|
||||||
|
{
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (Object item : objects) {
|
||||||
|
arr.add(convert(item));
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of a field, stripping off the leading underscore if present.
|
||||||
|
*/
|
||||||
|
public String getFieldName (Field field)
|
||||||
|
{
|
||||||
|
String name = field.getName();
|
||||||
|
if (name.startsWith("_")) {
|
||||||
|
name = name.substring(1);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Class<?>, List<Field>> cachedFields = Maps.newHashMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object to an array using an collection member. This covers the fairly common
|
||||||
|
* case where for example a {@code FooSet} needs to be stored as a json array of {@code Foo}
|
||||||
|
* instances, but for practical reasons the iterable is a member.
|
||||||
|
*/
|
||||||
|
public static class ConvertCollectionFieldToArray<T> implements Converter<T>
|
||||||
|
{
|
||||||
|
public final FieldAccessor<T, Collection<?>> accessor;
|
||||||
|
|
||||||
|
public ConvertCollectionFieldToArray (FieldAccessor<T, Collection<?>> accessor)
|
||||||
|
{
|
||||||
|
this.accessor = accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSON convert (T obj, Config cfg)
|
||||||
|
{
|
||||||
|
return cfg.toArray(accessor.get(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -45,7 +45,7 @@ public class BuildTestTileSetBundle
|
|||||||
// create our bundler and get going
|
// create our bundler and get going
|
||||||
TileSetBundler bundler = new TileSetBundler(configPath);
|
TileSetBundler bundler = new TileSetBundler(configPath);
|
||||||
File descFile = new File(descPath);
|
File descFile = new File(descPath);
|
||||||
bundler.createBundle(broker, descFile, targetPath);
|
bundler.process(broker, descFile, new BundleWriter(new File(targetPath))).create();
|
||||||
|
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
ioe.printStackTrace();
|
ioe.printStackTrace();
|
||||||
|
|||||||
Reference in New Issue
Block a user