From bce83d858e3e29d91f6236b528db9872cb64a342 Mon Sep 17 00:00:00 2001 From: Jamie Doornbos Date: Fri, 26 Oct 2012 21:45:06 -0700 Subject: [PATCH] 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. --- .../media/tile/bundle/BundleUtil.java | 8 +- tools/pom.xml | 7 + .../media/tile/bundle/tools/BundleWriter.java | 174 ++++++++ .../bundle/tools/DirectoryTileSetBundler.java | 171 -------- .../tools/DirectoryTileSetBundlerTask.java | 10 +- .../tile/bundle/tools/TileSetBundler.java | 280 ++++++------ .../tile/bundle/tools/TileSetBundlerTask.java | 25 +- .../com/threerings/tools/AWTConversions.java | 94 +++++ .../com/threerings/tools/JSONConversion.java | 399 ++++++++++++++++++ .../bundle/tools/BuildTestTileSetBundle.java | 2 +- 10 files changed, 844 insertions(+), 326 deletions(-) create mode 100644 tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java delete mode 100644 tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java create mode 100644 tools/src/main/java/com/threerings/tools/AWTConversions.java create mode 100644 tools/src/main/java/com/threerings/tools/JSONConversion.java diff --git a/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java b/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java index 13253b05..e4d2a920 100644 --- a/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java +++ b/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java @@ -35,10 +35,14 @@ import com.threerings.resource.ResourceBundle; */ public class BundleUtil { - /** The path to the metadata resource that we will attempt to load - * from our resource set. */ + /** The path to the binary metadata resource that jvm clients will attempt to load + * from the bundle's resource set. */ 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 * instance from the supplied resource bundle. diff --git a/tools/pom.xml b/tools/pom.xml index 8ccb51e8..4cde20fc 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -38,6 +38,13 @@ playn-core 1.5.1 + + net.sf.json-lib + json-lib + 2.4 + jdk15 + true + diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java new file mode 100644 index 00000000..1019a514 --- /dev/null +++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java @@ -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; +} diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java deleted file mode 100644 index 29f8c92c..00000000 --- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java +++ /dev/null @@ -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 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; - } -} diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java index 7e2ec172..b42317bb 100644 --- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java +++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java @@ -46,17 +46,11 @@ public class DirectoryTileSetBundlerTask extends TileSetBundlerTask } @Override - protected TileSetBundler createBundler () + protected BundleWriter createWriter (File fromDir, String path) throws IOException - { - return new DirectoryTileSetBundler(_config); - } - - @Override - protected String getTargetPath (File fromDir, String path) { 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. */ diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java index 1486dda3..4f0c4c3c 100644 --- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java +++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java @@ -21,21 +21,21 @@ package com.threerings.media.tile.bundle.tools; import java.util.ArrayList; 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.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; +import java.io.OutputStream; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; import com.google.common.collect.Lists; + +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; + import org.apache.commons.digester.Digester; import org.xml.sax.SAXException; @@ -45,6 +45,8 @@ import com.samskivert.util.HashIntMap; import com.threerings.resource.FastImageIO; +import com.threerings.tools.JSONConversion; + import com.threerings.media.tile.ImageProvider; import com.threerings.media.tile.ObjectTileSet; import com.threerings.media.tile.SimpleCachingImageProvider; @@ -112,6 +114,90 @@ import static com.threerings.media.Log.log; */ 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 * configuration file. The configuration file will be loaded and used @@ -130,19 +216,6 @@ public class TileSetBundler public TileSetBundler (File configFile) 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 Digester digester = new Digester(); @@ -198,46 +271,24 @@ public class TileSetBundler } /** - * Creates a tileset bundle at the location specified by the - * targetPath parameter, based on the description + * Prepares to create a tileset bundle at the location specified by the + * target parameter, based on the description * provided via the bundleDesc 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 targetPath the path of the tileset bundle file that will be - * created. + * @param target the tileset bundle file or directory that will be 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 * or processing anything. */ - public void createBundle ( - TileSetIDBroker idBroker, File bundleDesc, String targetPath) - throws IOException - { - createBundle(idBroker, bundleDesc, new File(targetPath)); - } - - /** - * Creates a tileset bundle at the location specified by the - * targetPath parameter, based on the description - * provided via the bundleDesc 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) + public Writer process ( + TileSetIDBroker idBroker, final File bundleDesc, BundleWriter target) throws IOException { // 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 - if (skipIfTargetNewer() && newest < target.lastModified()) { - return false; + if (target.isNewerThan(newest)) { + return null; } // 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. - * - * @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. + * Create the tileset bundle on disk using previously configured options. */ - public boolean createBundle ( - File target, TileSetBundle bundle, ImageProvider improv, String imageBase, long newestMod) + protected static void createBundle (Writer target) 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 { // write all of the image files to the bundle, converting the // tilesets to trimmed tilesets in the process - Iterator iditer = bundle.enumerateTileSetIds(); + Iterator iditer = target.bundle.enumerateTileSetIds(); // Store off the updated TileSets in a separate Map so we can wait to change the // bundle till we're done iterating. HashIntMap toUpdate = new HashIntMap(); while (iditer.hasNext()) { int tileSetId = iditer.next().intValue(); - TileSet set = bundle.getTileSet(tileSetId); + TileSet set = target.bundle.getTileSet(tileSetId); String imagePath = set.getImagePath(); // sanity checks @@ -395,22 +413,23 @@ public class TileSetBundler } // 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 // 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 - imagePath = adjustImagePath(imagePath); - jar.putNextEntry(new JarEntry(imagePath)); + // add .raw if requested + if (target.raw) { + imagePath = adjustImagePath(imagePath); + } + OutputStream dest = target.bwriter.startNewFile(imagePath); try { // create a trimmed object tileset, which will - // write the trimmed tileset image to the jar - // output stream + // write the trimmed tileset image to the target file TrimmedObjectTileSet tset = - TrimmedObjectTileSet.trimObjectTileSet( - (ObjectTileSet)set, jar); + TrimmedObjectTileSet.trimObjectTileSet((ObjectTileSet)set, dest, + target.raw ? FastImageIO.FILE_SUFFIX : "png"); tset.setImagePath(imagePath); // replace the original set with the trimmed // tileset in the tileset bundle @@ -427,18 +446,21 @@ public class TileSetBundler } else { // read the image file and convert it to our custom // format in the bundle - File ifile = new File(imageBase, imagePath); + File ifile = new File(target.imageBase, imagePath); try { BufferedImage image = ImageIO.read(ifile); - if (!keepOriginalPngs && FastImageIO.canWrite(image)) { + if (target.raw && FastImageIO.canWrite(image)) { 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); - FastImageIO.write(image, jar); + FastImageIO.write(image, dest); } else { - jar.putNextEntry(new JarEntry(imagePath)); + OutputStream dest = target.bwriter.startNewFile(imagePath); FileInputStream imgin = new FileInputStream(ifile); - StreamUtil.copy(imgin, jar); + StreamUtil.copy(imgin, dest); } } catch (Exception e) { 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 - // object to the bundle jar file - JarEntry entry = new JarEntry(BundleUtil.METADATA_PATH); - jar.putNextEntry(entry); - ObjectOutputStream oout = new ObjectOutputStream(jar); - oout.writeObject(bundle); - oout.flush(); + if (target.json != null) { + JSONArray array = new JSONArray(); + for (Iterator 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 + ObjectOutputStream oout = new ObjectOutputStream( + target.bwriter.startNewFile(BundleUtil.METADATA_PATH)); + oout.writeObject(target.bundle); + oout.flush(); + } // finally close up the jar file and call ourself done - jar.close(); - - return true; + target.bwriter.close(); } catch (Exception e) { // remove the incomplete jar file and rethrow the exception - jar.close(); - if (!target.delete()) { + target.bwriter.close(); + if (!target.bwriter.delete()) { log.warning("Failed to close botched bundle '" + target + "'."); } 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 .raw. */ protected static String adjustImagePath (String imagePath) { @@ -511,10 +539,4 @@ public class TileSetBundler /** The digester we use to parse bundle descriptions. */ 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; } diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java index a90f129c..25e9dbb5 100644 --- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java +++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java @@ -97,7 +97,7 @@ public class TileSetBundlerTask extends Task File cfile = null; try { // create a tileset bundler - TileSetBundler bundler = createBundler(); + TileSetBundler bundler = new TileSetBundler(_config); // create our tileset id broker MapFileTileSetIDBroker broker = @@ -123,16 +123,18 @@ public class TileSetBundlerTask extends Task "Config file should end with .xml."); continue; } - String bpath = getTargetPath(fromDir, cpath); - File bfile = new File(bpath); + BundleWriter bwriter = createWriter(fromDir, cpath); // 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( "Created bundle from '" + cpath + "'..."); } else { 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. */ - protected TileSetBundler createBundler () + protected BundleWriter createWriter (File fromDir, String path) throws IOException { - return new TileSetBundler(_config, _keepRawPngs, _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"; + return new BundleWriter( + new File(path.substring(0, path.length()-4) + ".jar"), _uncompressed); } protected void ensureSet (Object value, String errmsg) diff --git a/tools/src/main/java/com/threerings/tools/AWTConversions.java b/tools/src/main/java/com/threerings/tools/AWTConversions.java new file mode 100644 index 00000000..2c8867e8 --- /dev/null +++ b/tools/src/main/java/com/threerings/tools/AWTConversions.java @@ -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 = new Converter() { + 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 = new Converter() { + 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 = new Converter() { + 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 = new Converter() { + public Object convert (Dimension d, Config cfg) { + JSONArray out = new JSONArray(); + out.add(d.width); + out.add(d.height); + return out; + } + }; +} diff --git a/tools/src/main/java/com/threerings/tools/JSONConversion.java b/tools/src/main/java/com/threerings/tools/JSONConversion.java new file mode 100644 index 00000000..222e8c04 --- /dev/null +++ b/tools/src/main/java/com/threerings/tools/JSONConversion.java @@ -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.

+ * + * Example:
+ *    Config cfg = new Config();
+ *    System.out.println(cfg.convert(new Pojo()));
+ * 
+ * Converting a type universally:
+    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);
+ * 
+ * Customizing a specific field of a class:
+    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)));
+ * 
+ */ +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 + { + /** + * 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 + { + F get (T obj); + } + + /** + * Defines the field layout for serializing instances of {@code T}. + */ + public static class FieldConfig + { + /** + * Fields that should not be serialized. + */ + public List excludeFields = Lists.newArrayList(); + + /** + * Fields with custom conversions. + */ + public Map> customFields = Maps.newHashMap(); + + /** + * Adds some excluded fields and returns this for chaining. + */ + public FieldConfig addExclusions (String ...names) + { + excludeFields.addAll(Arrays.asList(names)); + return this; + } + + /** + * Adds a custom converter for a field and returns this for chaining. + */ + public FieldConfig addFieldConverter (String name, Converter 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, FieldConfig> classes = Maps.newHashMap(); + + /** + * Overrides the conversion process for encountered instances. + */ + public Map, Converter> converters = Maps.newHashMap(); + + /** + * Overrides the way fields are serialized for instances of {@code T} and returns this + * for chaining. + */ + public Config addFieldConfig (Class clazz, FieldConfig config) + { + classes.put(clazz, config); + return this; + } + + /** + * Overrides the way instances of a given type are serialized and returns this for chaining. + */ + public Config addClassConverter (Class clazz, Converter 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 converter = (Converter)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() { + 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 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 fieldConverter = config != null ? + (Converter)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() { + public Object run () + throws Exception + { + List 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 JSONArray toArray (Iterable 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, List> 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 implements Converter + { + public final FieldAccessor> accessor; + + public ConvertCollectionFieldToArray (FieldAccessor> accessor) + { + this.accessor = accessor; + } + + public JSON convert (T obj, Config cfg) + { + return cfg.toArray(accessor.get(obj)); + } + } +} diff --git a/tools/src/test/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java b/tools/src/test/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java index b7928bfb..2cac498b 100644 --- a/tools/src/test/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java +++ b/tools/src/test/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java @@ -45,7 +45,7 @@ public class BuildTestTileSetBundle // create our bundler and get going TileSetBundler bundler = new TileSetBundler(configPath); File descFile = new File(descPath); - bundler.createBundle(broker, descFile, targetPath); + bundler.process(broker, descFile, new BundleWriter(new File(targetPath))).create(); } catch (IOException ioe) { ioe.printStackTrace();