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:
Jamie Doornbos
2012-10-26 21:45:06 -07:00
committed by Michael Bayne
parent 426165b85e
commit bce83d858e
10 changed files with 844 additions and 326 deletions
@@ -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.
+7
View File
@@ -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;
}
@@ -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;
}
}
@@ -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;
} }
@@ -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&lt;Color&gt; colorConv = new Converter&lt;Color&gt;() {
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&lt;SomeImpl, Collection&lt;?&gt;&gt; entryConv =
new FieldAccessor&lt;SomeImpl, Collection&lt;?&gt;&gt;() {
public Collection&lt;SomeValue&gt; get (SomeImpl impl) {
return impl.getValuesAsCollection();
}
};
cfg.addFieldConfig(SomeImpl.class, new FieldConfig&lt;SomeImpl&gt;().
addExclusions("privateStuff").
addFieldConverter("valueMap", new ConvertCollectionFieldToArray&lt;SomeImpl&gt;(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));
}
}
}
@@ -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();