Java tutorial
/* * This software copyright by various authors including the RPTools.net * development team, and licensed under the LGPL Version 3 or, at your option, * any later version. * * Portions of this software were originally covered under the Apache Software * License, Version 1.1 or Version 2.0. * * See the file LICENSE elsewhere in this distribution for license details. */ package net.rptools.maptool.util; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.imageio.ImageIO; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import com.caucho.hessian.io.HessianInput; // import com.google.common.io.Files; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.ConversionException; import net.rptools.lib.CodeTimer; import net.rptools.lib.FileUtil; import net.rptools.lib.MD5Key; import net.rptools.lib.ModelVersionManager; import net.rptools.lib.image.ImageUtil; import net.rptools.lib.io.PackedFile; import net.rptools.lib.swing.SwingUtil; import net.rptools.maptool.client.AppConstants; import net.rptools.maptool.client.AppUtil; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ui.Scale; import net.rptools.maptool.client.ui.zone.PlayerView; import net.rptools.maptool.client.ui.zone.ZoneRenderer; import net.rptools.maptool.model.Asset; import net.rptools.maptool.model.AssetManager; import net.rptools.maptool.model.Campaign; import net.rptools.maptool.model.CampaignProperties; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.LookupTable; import net.rptools.maptool.model.MacroButtonProperties; import net.rptools.maptool.model.Token; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.transform.campaign.AssetNameTransform; import net.rptools.maptool.model.transform.campaign.ExportInfoTransform; import net.rptools.maptool.model.transform.campaign.PCVisionTransform; import net.rptools.maptool.model.transform.campaign.TokenPropertyMapTransform; /** * @author trevor */ public class PersistenceUtil { private static final Logger log = Logger.getLogger(PersistenceUtil.class); private static final String PROP_VERSION = "version"; //$NON-NLS-1$ private static final String PROP_CAMPAIGN_VERSION = "campaignVersion"; //$NON-NLS-1$ private static final String ASSET_DIR = "assets/"; //$NON-NLS-1$ private static final String CAMPAIGN_VERSION = "1.3.85"; // Please add a single note regarding why the campaign version number has been updated: // 1.3.70 ownerOnly added to model.Light (not backward compatible) // 1.3.75 model.Token.visibleOnlyToOwner (actually added to b74 but I didn't catch it before release) // 1.3.83 ExposedAreaData added to tokens in b78 but again not caught until b82 :( // 1.3.85 Added CampaignProperties.hasUsedFogToolbar (old versions could ignore this field, but how to implement?) private static final ModelVersionManager campaignVersionManager = new ModelVersionManager(); private static final ModelVersionManager assetnameVersionManager = new ModelVersionManager(); private static final ModelVersionManager tokenVersionManager = new ModelVersionManager(); static { PackedFile.init(AppUtil.getAppHome("tmp")); //$NON-NLS-1$ // Whenever a new transformation needs to be added, put the version of MT into the CAMPAIGN_VERSION // variable, and use that as the key to the following register call. // This gives us a rough estimate how far backwards compatible the model is. // If you need sub-minor version level granularity, simply add another dot value at the end (e.g. 1.3.51.1) // To be clear: the transformation will be applied if the file version is < the version number provided. // Or to put it another way, the version you register should probably be equal to // the new value of CAMPAIGN_VERSION as of the time of your code changes. // FIXME We should be using javax.xml.transform to do XSL transforms with a mechanism // that allows the XSL to be stored externally, perhaps via a URL with version number(s) // as parameters. Then if some backward compatibility fix is needed it could be // provided dynamically via the RPTools.net web site or somewhere else. We'll add this // in 1.4 <wink, wink> // Note: This only allows you to fix up outdated XML data. If you _added_ // variables to any persistent class which must be initialized, you need to make sure to // modify the object's readResolve() function, because XStream does _not_ call the // regular constructors! Using factory methods won't help here, since it won't be called by XStream. // Notes: any XML earlier than this ---v will have this --v applied to it // V V campaignVersionManager.registerTransformation("1.3.51", new PCVisionTransform()); campaignVersionManager.registerTransformation("1.3.75", new ExportInfoTransform()); campaignVersionManager.registerTransformation("1.3.78", new TokenPropertyMapTransform()); // FJE 2010-12-29 // For a short time, assets were stored separately in files ending with ".dat". As of 1.3.64, they are // stored in separate files using the correct filename extension for the image type. This transform // is used to convert asset filenames and not XML. Old assets with the image embedded as Base64 // text are still supported for reading by using an XStream custom Converter. See the Asset // class for the annotation used to reference the converter. assetnameVersionManager.registerTransformation("1.3.51", new AssetNameTransform("^(.*)\\.(dat)?$", "$1")); // This version manager is only for loading and saving tokens. Note that many (all?) of its contents will // be used by the campaign version manager since campaigns contain tokens... tokenVersionManager.registerTransformation("1.3.78", new TokenPropertyMapTransform()); // FJE 2010-12-29 } public static class PersistedMap { public Zone zone; public Map<MD5Key, Asset> assetMap = new HashMap<MD5Key, Asset>(); public String mapToolVersion; } public static class PersistedCampaign { public Campaign campaign; public Map<MD5Key, Asset> assetMap = new HashMap<MD5Key, Asset>(); public GUID currentZoneId; public Scale currentView; public String mapToolVersion; } public static void saveMap(Zone z, File mapFile) throws IOException { PersistedMap pMap = new PersistedMap(); pMap.zone = z; // Save all assets in active use (consolidate duplicates) Set<MD5Key> allAssetIds = z.getAllAssetIds(); for (MD5Key key : allAssetIds) { // Put in a placeholder pMap.assetMap.put(key, null); } PackedFile pakFile = null; try { pakFile = new PackedFile(mapFile); saveAssets(z.getAllAssetIds(), pakFile); pakFile.setContent(pMap); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.setProperty(PROP_CAMPAIGN_VERSION, CAMPAIGN_VERSION); pakFile.save(); } finally { if (pakFile != null) pakFile.close(); } } public static PersistedMap loadMap(File mapFile) throws IOException { PackedFile pakFile = null; try { pakFile = new PackedFile(mapFile); // Sanity check String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; PersistedMap persistedMap = (PersistedMap) pakFile.getContent(); // Now load up any images that we need loadAssets(persistedMap.assetMap.keySet(), pakFile); // FJE We only want the token's graphical data, so loop through all tokens and // destroy all properties and macros. Keep some fields, though. Since that type // of object editing doesn't belong here, we just call Token.imported() and let // that method Do The Right Thing. for (Iterator<Token> iter = persistedMap.zone.getAllTokens().iterator(); iter.hasNext();) { Token token = iter.next(); token.imported(); } // XXX FJE This doesn't work the way I want it to. But doing this the Right Way // is too much work right now. :-} Zone z = persistedMap.zone; String n = fixupZoneName(z.getName()); z.setName(n); z.imported(); // Resets creation timestamp and init panel, among other things z.optimize(); // Collapses overlaid or redundant drawables return persistedMap; } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.mapVersion", ce); } catch (IOException ioe) { MapTool.showError("PersistenceUtil.error.mapRead", ioe); } finally { if (pakFile != null) pakFile.close(); } return null; } /** * Determines whether the incoming map name is unique. If it is, it's * returned as-is. If it's not unique, a newly generated name is returned. * * @param n * name from imported map * @return new name to use for the map */ private static String fixupZoneName(String n) { List<Zone> zones = MapTool.getCampaign().getZones(); for (Zone zone : zones) { if (zone.getName().equals(n)) { String count = n.replaceFirst("Import (\\d+) of.*", "$1"); //$NON-NLS-1$ Integer next = 1; try { next = StringUtil.parseInteger(count) + 1; n = n.replaceFirst("Import \\d+ of", "Import " + next + " of"); //$NON-NLS-1$ } catch (ParseException e) { n = "Import " + next + " of " + n; //$NON-NLS-1$ } } } return n; } public static void saveCampaign(Campaign campaign, File campaignFile) throws IOException { CodeTimer saveTimer; // FJE Previously this was 'private static' -- why? saveTimer = new CodeTimer("CampaignSave"); saveTimer.setThreshold(5); saveTimer.setEnabled(log.isDebugEnabled()); // Don't bother keeping track if it won't be displayed... // Strategy: save the file to a tmp location so that if there's a failure the original file // won't be touched. Then once we're finished, replace the old with the new. File tmpDir = AppUtil.getTmpDir(); File tmpFile = new File(tmpDir.getAbsolutePath(), campaignFile.getName()); if (tmpFile.exists()) tmpFile.delete(); PackedFile pakFile = null; try { pakFile = new PackedFile(tmpFile); // Configure the meta file (this is for legacy support) PersistedCampaign persistedCampaign = new PersistedCampaign(); persistedCampaign.campaign = campaign; // Keep track of the current view ZoneRenderer currentZoneRenderer = MapTool.getFrame().getCurrentZoneRenderer(); if (currentZoneRenderer != null) { persistedCampaign.currentZoneId = currentZoneRenderer.getZone().getId(); persistedCampaign.currentView = currentZoneRenderer.getZoneScale(); } // Save all assets in active use (consolidate duplicates between maps) saveTimer.start("Collect all assets"); Set<MD5Key> allAssetIds = campaign.getAllAssetIds(); for (MD5Key key : allAssetIds) { // Put in a placeholder; all we really care about is the MD5Key for now... persistedCampaign.assetMap.put(key, null); } saveTimer.stop("Collect all assets"); // And store the asset elsewhere saveTimer.start("Save assets"); saveAssets(allAssetIds, pakFile); saveTimer.stop("Save assets"); try { saveTimer.start("Set content"); pakFile.setContent(persistedCampaign); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.setProperty(PROP_CAMPAIGN_VERSION, CAMPAIGN_VERSION); saveTimer.stop("Set content"); saveTimer.start("Save"); pakFile.save(); saveTimer.stop("Save"); } catch (OutOfMemoryError oom) { /* * This error is normally because the heap space has been * exceeded while trying to save the campaign. Since MapTool * caches the images used by the current Zone, and since the * VersionManager must keep the XML for objects in memory in * order to apply transforms to them, the memory usage can spike * very high during the save() operation. A common solution is * to switch to an empty map and perform the save from there; * this causes MapTool to unload any images that it may have had * cached and this can frequently free up enough memory for the * save() to work. We'll tell the user all this right here and * then fail the save and they can try again. */ saveTimer.start("OOM Close"); pakFile.close(); // Have to close the tmpFile first on some OSes pakFile = null; tmpFile.delete(); // Delete the temporary file saveTimer.stop("OOM Close"); if (log.isDebugEnabled()) { log.debug(saveTimer); } MapTool.showError("msg.error.failedSaveCampaignOOM"); return; } } finally { saveTimer.start("Close"); try { if (pakFile != null) pakFile.close(); } catch (Exception e) { } saveTimer.stop("Close"); pakFile = null; } /* * Copy to the new location. Not the fastest solution in the world if * renameTo() fails, but worth the safety net it provides. (Jamz had * issues with renameTo() keeping DropBox files locked on Windows. * Changed to use Files.move() from Java 7.) */ saveTimer.start("Backup"); File bakFile = new File(tmpDir.getAbsolutePath(), campaignFile.getName() + ".bak"); bakFile.delete(); if (campaignFile.exists()) { saveTimer.start("Backup campaign file: " + campaignFile); try { Files.move(campaignFile.toPath(), bakFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception ex) { try { FileUtil.copyFile(campaignFile, bakFile); } catch (Exception e) { MapTool.showError("msg.error.failedSaveCampaign"); return; } } finally { saveTimer.stop("Backup campaign file: " + campaignFile); } } saveTimer.start("Backup tmpFile to campaign file"); try { Files.move(tmpFile.toPath(), campaignFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { FileUtil.copyFile(campaignFile, bakFile); tmpFile.delete(); // Only delete if the copy didn't throw an exception } saveTimer.stop("Backup tmpFile to campaign file"); bakFile.delete(); saveTimer.stop("Backup"); // Save the campaign thumbnail saveTimer.start("Thumbnail"); saveCampaignThumbnail(campaignFile.getName()); saveTimer.stop("Thumbnail"); if (log.isDebugEnabled()) { log.debug(saveTimer); } } /* * A public function because I think it should be called when a campaign is * opened as well so if it is opened then closed without saving, there is * still a preview created; however, the rendering of the campaign appears * to complete after AppActions.loadCampaign returns, causing the preview to * always appear as black if this method is called from within loadCampaign. * Either need to find another place to call saveCampaignThumbnail upon * opening, or code to delay it's call until the render is complete. =P */ static public void saveCampaignThumbnail(String fileName) { BufferedImage screen = MapTool.takeMapScreenShot(new PlayerView(MapTool.getPlayer().getRole())); if (screen == null) return; Dimension imgSize = new Dimension(screen.getWidth(null), screen.getHeight(null)); SwingUtil.constrainTo(imgSize, 200, 200); BufferedImage thumb = new BufferedImage(imgSize.width, imgSize.height, BufferedImage.TYPE_INT_BGR); Graphics2D g2d = thumb.createGraphics(); g2d.drawImage(screen, 0, 0, imgSize.width, imgSize.height, null); g2d.dispose(); File thumbFile = getCampaignThumbnailFile(fileName); try { ImageIO.write(thumb, "jpg", thumbFile); } catch (IOException ioe) { MapTool.showError("msg.error.failedSaveCampaignPreview", ioe); } } /** * Gets a file pointing to where the campaign's thumbnail image should be. * * @param fileName * The campaign's file name. */ public static File getCampaignThumbnailFile(String fileName) { return new File(AppUtil.getAppHome("campaignthumbs"), fileName + ".jpg"); } public static PersistedCampaign loadCampaign(File campaignFile) throws IOException { PersistedCampaign persistedCampaign = null; // Try the new way first PackedFile pakFile = null; try { pakFile = new PackedFile(campaignFile); pakFile.setModelVersionManager(campaignVersionManager); // Sanity check String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; String campaignVersion = (String) pakFile.getProperty(PROP_CAMPAIGN_VERSION); // This is where the campaignVersion was added campaignVersion = campaignVersion == null ? "1.3.50" : campaignVersion; try { persistedCampaign = (PersistedCampaign) pakFile.getContent(campaignVersion); } catch (ConversionException ce) { // Ignore the exception and check for "campaign == null" below... MapTool.showError("PersistenceUtil.error.campaignVersion", ce); } if (persistedCampaign != null) { // Now load up any images that we need // Note that the values are all placeholders Set<MD5Key> allAssetIds = persistedCampaign.assetMap.keySet(); loadAssets(allAssetIds, pakFile); for (Zone zone : persistedCampaign.campaign.getZones()) { zone.optimize(); } return persistedCampaign; } } catch (OutOfMemoryError oom) { MapTool.showError("Out of memory while reading campaign.", oom); return null; } catch (RuntimeException rte) { MapTool.showError("PersistenceUtil.error.campaignRead", rte); } catch (Error e) { // Probably an issue with XStream not being able to instantiate a given class // The old legacy technique probably won't work, but we should at least try... MapTool.showError("PersistenceUtil.error.unknown", e); } finally { if (pakFile != null) pakFile.close(); } log.warn("Could not load campaign in the current format... trying the legacy format."); persistedCampaign = loadLegacyCampaign(campaignFile); if (persistedCampaign == null) MapTool.showWarning("PersistenceUtil.warn.campaignNotLoaded"); return persistedCampaign; } public static PersistedCampaign loadLegacyCampaign(File campaignFile) { HessianInput his = null; PersistedCampaign persistedCampaign = null; try { InputStream is = new BufferedInputStream(new FileInputStream(campaignFile)); his = new HessianInput(is); persistedCampaign = (PersistedCampaign) his.readObject(null); for (MD5Key key : persistedCampaign.assetMap.keySet()) { Asset asset = persistedCampaign.assetMap.get(key); if (!AssetManager.hasAsset(key)) AssetManager.putAsset(asset); if (!MapTool.isHostingServer() && !MapTool.isPersonalServer()) { // If we are remotely installing this campaign, we'll need to // send the image data to the server MapTool.serverCommand().putAsset(asset); } } // Do some sanity work on the campaign // This specifically handles the case when the zone mappings // are out of sync in the save file Campaign campaign = persistedCampaign.campaign; Set<Zone> zoneSet = new HashSet<Zone>(campaign.getZones()); campaign.removeAllZones(); for (Zone zone : zoneSet) { campaign.putZone(zone); } } catch (FileNotFoundException fnfe) { if (log.isInfoEnabled()) log.info("Campaign file not found -- this can't happen?!", fnfe); persistedCampaign = null; } catch (IOException ioe) { if (log.isInfoEnabled()) log.info("Campaign is not in legacy Hessian format either.", ioe); persistedCampaign = null; } finally { try { his.close(); } catch (Exception e) { } } return persistedCampaign; } public static BufferedImage getTokenThumbnail(File file) throws Exception { PackedFile pakFile = new PackedFile(file); BufferedImage thumb; try { thumb = null; if (pakFile.hasFile(Token.FILE_THUMBNAIL)) { InputStream is = null; try { is = pakFile.getFileAsInputStream(Token.FILE_THUMBNAIL); thumb = ImageIO.read(is); } finally { IOUtils.closeQuietly(is); } } } finally { pakFile.close(); } return thumb; } public static void saveToken(Token token, File file) throws IOException { saveToken(token, file, false); } public static void saveToken(Token token, File file, boolean doWait) throws IOException { // Thumbnail BufferedImage image = null; if (doWait) image = ImageManager.getImageAndWait(token.getImageAssetId()); else image = ImageManager.getImage(token.getImageAssetId()); Dimension sz = new Dimension(image.getWidth(), image.getHeight()); SwingUtil.constrainTo(sz, 50); BufferedImage thumb = new BufferedImage(sz.width, sz.height, BufferedImage.TRANSLUCENT); Graphics2D g = thumb.createGraphics(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(image, 0, 0, sz.width, sz.height, null); g.dispose(); PackedFile pakFile = null; try { pakFile = new PackedFile(file); saveAssets(token.getAllImageAssets(), pakFile); pakFile.putFile(Token.FILE_THUMBNAIL, ImageUtil.imageToBytes(thumb, "png")); pakFile.setContent(token); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.save(); } finally { if (pakFile != null) pakFile.close(); } } public static Token loadToken(File file) throws IOException { PackedFile pakFile = null; Token token = null; try { pakFile = new PackedFile(file); pakFile.setModelVersionManager(tokenVersionManager); // Sanity check String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; token = (Token) pakFile.getContent(progVersion); loadAssets(token.getAllImageAssets(), pakFile); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.tokenVersion", ce); } catch (IOException ioe) { MapTool.showError("PersistenceUtil.error.tokenRead", ioe); } if (pakFile != null) pakFile.close(); return token; } public static Token loadToken(URL url) throws IOException { // Create a temporary file from the downloaded URL File newFile = new File(PackedFile.getTmpDir(), new GUID() + ".url"); FileUtils.copyURLToFile(url, newFile); Token token = loadToken(newFile); newFile.delete(); return token; } private static void loadAssets(Collection<MD5Key> assetIds, PackedFile pakFile) throws IOException { // Special handling of assets: XML file to describe the Asset, but binary file for the image data pakFile.getXStream().processAnnotations(Asset.class); String campaignVersion = (String) pakFile.getProperty(PROP_CAMPAIGN_VERSION); String progVersion = (String) pakFile.getProperty(PROP_VERSION); List<Asset> addToServer = new ArrayList<Asset>(assetIds.size()); // FJE: Ugly fix for a bug I introduced in b64. :( boolean fixRequired = "1.3.b64".equals(progVersion); for (MD5Key key : assetIds) { if (key == null) continue; if (!AssetManager.hasAsset(key)) { String pathname = ASSET_DIR + key; Asset asset = null; if (fixRequired) { InputStream is = null; try { is = pakFile.getFileAsInputStream(pathname); asset = new Asset(key.toString(), IOUtils.toByteArray(is)); // Ugly bug fix :( } catch (FileNotFoundException fnf) { // Doesn't need to be reported, since that's handled below. } catch (Exception e) { log.error("Could not load asset from 1.3.b64 file in compatibility mode", e); } finally { IOUtils.closeQuietly(is); } } else { try { asset = (Asset) pakFile.getFileObject(pathname); // XML deserialization } catch (Exception e) { // Do nothing. The asset will be 'null' and it'll be handled below. log.info("Exception while handling asset '" + pathname + "'", e); } } if (asset == null) { // Referenced asset not included in PackedFile?? log.error("Referenced asset '" + pathname + "' not found while loading?!"); continue; } // If the asset was marked as "broken" then ignore it completely. The end // result is that MT will attempt to load it from a repository again, as normal. if ("broken".equals(asset.getName())) { log.warn("Reference to 'broken' asset '" + pathname + "' not restored."); ImageManager.flushImage(asset); continue; } // pre 1.3b52 campaign files stored the image data directly in the asset serialization. // New XStreamConverter creates empty byte[] for image. if (asset.getImage() == null || asset.getImage().length < 4) { String ext = asset.getImageExtension(); pathname = pathname + "." + (StringUtil.isEmpty(ext) ? "dat" : ext); pathname = assetnameVersionManager.transform(pathname, campaignVersion); InputStream is = null; try { is = pakFile.getFileAsInputStream(pathname); asset.setImage(IOUtils.toByteArray(is)); } catch (FileNotFoundException fnf) { log.error("Image data for '" + pathname + "' not found?!", fnf); continue; } catch (Exception e) { log.error("While reading image data for '" + pathname + "'", e); continue; } finally { IOUtils.closeQuietly(is); } } AssetManager.putAsset(asset); addToServer.add(asset); } } if (!addToServer.isEmpty()) { // Isn't this the same as (MapTool.getServer() == null) ? And won't there always // be a server? Even if we don't start one explicitly, MapTool keeps a server // running in the background all the time (called a "personal server") so that the rest // of the code is consistent with regard to client<->server operations... boolean server = !MapTool.isHostingServer() && !MapTool.isPersonalServer(); if (server) { if (MapTool.isDevelopment()) MapTool.showInformation( "Please report this: (!isHostingServer() && !isPersonalServer()) == true"); // If we are remotely installing this token, we'll need to send the image data to the server. for (Asset asset : addToServer) { MapTool.serverCommand().putAsset(asset); } } addToServer.clear(); } } private static void saveAssets(Collection<MD5Key> assetIds, PackedFile pakFile) throws IOException { // Special handling of assets: XML file to describe the Asset, but binary file for the image data pakFile.getXStream().processAnnotations(Asset.class); for (MD5Key assetId : assetIds) { if (assetId == null) continue; // And store the asset elsewhere // As of 1.3.b64, assets are written in binary to allow them to be readable // when a campaign file is unpacked. Asset asset = AssetManager.getAsset(assetId); if (asset == null) { log.error("AssetId " + assetId + " not found while saving?!"); continue; } pakFile.putFile(ASSET_DIR + assetId + "." + asset.getImageExtension(), asset.getImage()); pakFile.putFile(ASSET_DIR + assetId, asset); // Does not write the image } } private static void clearAssets(PackedFile pakFile) throws IOException { for (String path : pakFile.getPaths()) { if (path.startsWith(ASSET_DIR) && !path.equals(ASSET_DIR)) pakFile.removeFile(path); } } public static CampaignProperties loadLegacyCampaignProperties(File file) throws IOException { if (!file.exists()) throw new FileNotFoundException(); FileInputStream in = new FileInputStream(file); try { return loadCampaignProperties(in); } finally { IOUtils.closeQuietly(in); } } public static CampaignProperties loadCampaignProperties(InputStream in) throws IOException { CampaignProperties props = null; try { props = (CampaignProperties) new XStream().fromXML(new InputStreamReader(in, "UTF-8")); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.campaignPropertiesVersion", ce); } return props; } /** * Answers the question, "Can this version of MapTool load an XML file with * a version string of <code>progVersion</code>?" * * @param progVersion * version string read from the XML file * @return <code>true</code> if this MT can read the file based on the * version string, <code>false</code> if it can't. */ private static boolean versionCheck(String progVersion) { boolean okay = true; String mtversion = ModelVersionManager.cleanVersionNumber(MapTool.getVersion()); String cleanedProgVersion = ModelVersionManager.cleanVersionNumber(progVersion); // uses "0" if version is null // If this version of MapTool (check added in 1.3b78) is earlier than the one that created the file, warn the user. :) if (!MapTool.isDevelopment() && ModelVersionManager.isBefore(mtversion, cleanedProgVersion)) { // Give the user a chance to abort this attempt to load the file okay = MapTool.confirm("msg.confirm.newerVersion", MapTool.getVersion(), progVersion); } return okay; } public static CampaignProperties loadCampaignProperties(File file) { PackedFile pakFile = null; try { pakFile = new PackedFile(file); String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; CampaignProperties props = null; try { props = (CampaignProperties) pakFile.getContent(); loadAssets(props.getAllImageAssets(), pakFile); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.campaignPropertiesVersion", ce); } catch (IOException ioe) { MapTool.showError("Could not load campaign properties", ioe); } return props; } catch (IOException e) { try { if (pakFile != null) pakFile.close(); // first close PackedFile (if it was opened) 'cuz some stupid OSes won't allow a file to be opened twice (ugh). pakFile = null; return loadLegacyCampaignProperties(file); } catch (IOException ioe) { MapTool.showError("PersistenceUtil.error.campaignPropertiesLegacy", ioe); } } return null; } public static void saveCampaignProperties(Campaign campaign, File file) throws IOException { // Put this in FileUtil if (file.getName().indexOf(".") < 0) { file = new File(file.getAbsolutePath() + AppConstants.CAMPAIGN_PROPERTIES_FILE_EXTENSION); } PackedFile pakFile = null; try { pakFile = new PackedFile(file); clearAssets(pakFile); saveAssets(campaign.getCampaignProperties().getAllImageAssets(), pakFile); pakFile.setContent(campaign.getCampaignProperties()); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.save(); } finally { if (pakFile != null) pakFile.close(); } } // Macro import/export support public static MacroButtonProperties loadLegacyMacro(File file) throws IOException { if (!file.exists()) throw new FileNotFoundException(); FileInputStream in = new FileInputStream(file); try { return loadMacro(in); } finally { IOUtils.closeQuietly(in); } } public static MacroButtonProperties loadMacro(InputStream in) throws IOException { MacroButtonProperties mbProps = null; try { mbProps = (MacroButtonProperties) new XStream().fromXML(new InputStreamReader(in, "UTF-8")); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.macroVersion", ce); } catch (IOException ioe) { MapTool.showError("PersistenceUtil.error.macroRead", ioe); } return mbProps; } public static MacroButtonProperties loadMacro(File file) throws IOException { PackedFile pakFile = null; try { pakFile = new PackedFile(file); // Sanity check String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; MacroButtonProperties macroButton = (MacroButtonProperties) pakFile.getContent(); return macroButton; } catch (IOException e) { if (pakFile != null) pakFile.close(); pakFile = null; return loadLegacyMacro(file); } finally { if (pakFile != null) pakFile.close(); } } public static void saveMacro(MacroButtonProperties macroButton, File file) throws IOException { // Put this in FileUtil if (file.getName().indexOf(".") < 0) { file = new File(file.getAbsolutePath() + AppConstants.MACRO_FILE_EXTENSION); } PackedFile pakFile = null; try { pakFile = new PackedFile(file); pakFile.setContent(macroButton); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.save(); } finally { if (pakFile != null) pakFile.close(); } } public static List<MacroButtonProperties> loadLegacyMacroSet(File file) throws IOException { if (!file.exists()) { throw new FileNotFoundException(); } FileInputStream in = new FileInputStream(file); try { return loadMacroSet(in); } finally { IOUtils.closeQuietly(in); } } @SuppressWarnings("unchecked") public static List<MacroButtonProperties> loadMacroSet(InputStream in) throws IOException { List<MacroButtonProperties> macroButtonSet = null; try { macroButtonSet = (List<MacroButtonProperties>) new XStream() .fromXML(new InputStreamReader(in, "UTF-8")); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.macrosetVersion", ce); } return macroButtonSet; } @SuppressWarnings("unchecked") public static List<MacroButtonProperties> loadMacroSet(File file) throws IOException { PackedFile pakFile = null; List<MacroButtonProperties> macroButtonSet = null; try { pakFile = new PackedFile(file); // Sanity check String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; macroButtonSet = (List<MacroButtonProperties>) pakFile.getContent(); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.macrosetVersion", ce); } catch (IOException e) { return loadLegacyMacroSet(file); } finally { if (pakFile != null) pakFile.close(); } return macroButtonSet; } public static void saveMacroSet(List<MacroButtonProperties> macroButtonSet, File file) throws IOException { // Put this in FileUtil if (file.getName().indexOf(".") < 0) { file = new File(file.getAbsolutePath() + AppConstants.MACROSET_FILE_EXTENSION); } PackedFile pakFile = null; try { pakFile = new PackedFile(file); pakFile.setContent(macroButtonSet); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.save(); } finally { if (pakFile != null) pakFile.close(); } } // end of Macro import/export support // Table import/export support public static LookupTable loadLegacyTable(File file) throws IOException { if (!file.exists()) throw new FileNotFoundException(); FileInputStream in = new FileInputStream(file); try { return loadTable(in); } finally { IOUtils.closeQuietly(in); } } public static LookupTable loadTable(InputStream in) { LookupTable table = null; try { table = (LookupTable) new XStream().fromXML(new InputStreamReader(in, "UTF-8")); } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.tableVersion", ce); } catch (IOException ioe) { MapTool.showError("PersistenceUtil.error.tableRead", ioe); } return table; } public static LookupTable loadTable(File file) throws IOException { PackedFile pakFile = null; try { pakFile = new PackedFile(file); // Sanity check String progVersion = (String) pakFile.getProperty(PROP_VERSION); if (!versionCheck(progVersion)) return null; LookupTable lookupTable = (LookupTable) pakFile.getContent(); loadAssets(lookupTable.getAllAssetIds(), pakFile); return lookupTable; } catch (ConversionException ce) { MapTool.showError("PersistenceUtil.error.tableVersion", ce); } catch (IOException e) { try { if (pakFile != null) pakFile.close(); pakFile = null; return loadLegacyTable(file); } catch (IOException ioe) { MapTool.showError("PersistenceUtil.error.tableRead", ioe); } } finally { if (pakFile != null) pakFile.close(); } return null; } public static void saveTable(LookupTable lookupTable, File file) throws IOException { // Put this in FileUtil if (file.getName().indexOf(".") < 0) { file = new File(file.getAbsolutePath() + AppConstants.TABLE_FILE_EXTENSION); } PackedFile pakFile = null; try { pakFile = new PackedFile(file); pakFile.setContent(lookupTable); saveAssets(lookupTable.getAllAssetIds(), pakFile); pakFile.setProperty(PROP_VERSION, MapTool.getVersion()); pakFile.save(); } finally { if (pakFile != null) pakFile.close(); } } }