/*
* Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, version 2.0. If a copy of the MPL was not distributed with
* this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, version 2.0.
*/
package com.trollworks.gcs.common;
import com.trollworks.toolkit.io.Log;
import com.trollworks.toolkit.io.SafeFileUpdater;
import com.trollworks.toolkit.io.xml.XMLNodeType;
import com.trollworks.toolkit.io.xml.XMLReader;
import com.trollworks.toolkit.io.xml.XMLWriter;
import com.trollworks.toolkit.ui.image.StdImageSet;
import com.trollworks.toolkit.ui.menu.edit.Undoable;
import com.trollworks.toolkit.ui.widget.DataModifiedListener;
import com.trollworks.toolkit.utility.FileType;
import com.trollworks.toolkit.utility.PathUtils;
import com.trollworks.toolkit.utility.UniqueID;
import com.trollworks.toolkit.utility.VersionException;
import com.trollworks.toolkit.utility.notification.Notifier;
import com.trollworks.toolkit.utility.notification.NotifierTarget;
import com.trollworks.toolkit.utility.undo.StdUndoManager;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.UUID;
import javax.swing.undo.UndoableEdit;
/** A common super class for all data file-based model objects. */
public abstract class DataFile implements Undoable {
/** The 'id' attribute. */
public static final String ATTRIBUTE_ID = "id"; //$NON-NLS-1$
private File mFile;
private UUID mId = UUID.randomUUID();
private Notifier mNotifier = new Notifier();
private boolean mModified;
private StdUndoManager mUndoManager = new StdUndoManager();
private ArrayList<DataModifiedListener> mDataModifiedListeners = new ArrayList<>();
private boolean mSortingMarksDirty = true;
/** @param file The file to load. */
public void load(File file) throws IOException {
setFile(file);
try (FileReader fileReader = new FileReader(file)) {
try (XMLReader reader = new XMLReader(fileReader)) {
XMLNodeType type = reader.next();
boolean found = false;
while (type != XMLNodeType.END_DOCUMENT) {
if (type == XMLNodeType.START_TAG) {
String name = reader.getName();
if (matchesRootTag(name)) {
if (!found) {
found = true;
load(reader, new LoadState());
} else {
throw new IOException();
}
} else {
reader.skipTag(name);
}
type = reader.getType();
} else {
type = reader.next();
}
}
}
}
mModified = false;
}
/**
* @param reader The {@link XMLReader} to load data from.
* @param state The {@link LoadState} to use.
*/
public void load(XMLReader reader, LoadState state) throws IOException {
try {
mId = UUID.fromString(reader.getAttribute(ATTRIBUTE_ID));
} catch (Exception exception) {
mId = UUID.randomUUID();
}
state.mDataFileVersion = reader.getAttributeAsInteger(LoadState.ATTRIBUTE_VERSION, 0);
if (state.mDataFileVersion > getXMLTagVersion()) {
throw VersionException.createTooNew();
}
loadSelf(reader, state);
}
/**
* Called to load the data file.
*
* @param reader The {@link XMLReader} to load data from.
* @param state The {@link LoadState} to use.
*/
protected abstract void loadSelf(XMLReader reader, LoadState state) throws IOException;
/**
* Saves the data out to the specified file. Does not affect the result of {@link #getFile()}.
*
* @param file The file to write to.
* @return <code>true</code> on success.
*/
public boolean save(File file) {
SafeFileUpdater transaction = new SafeFileUpdater();
boolean success = false;
transaction.begin();
try {
File transactionFile = transaction.getTransactionFile(file);
try (XMLWriter out = new XMLWriter(new BufferedOutputStream(new FileOutputStream(transactionFile)))) {
out.writeHeader();
save(out, true, false);
success = !out.checkError();
}
if (success) {
success = false;
transaction.commit();
setModified(false);
success = true;
} else {
transaction.abort();
}
} catch (Exception exception) {
Log.error(exception);
transaction.abort();
}
return success;
}
/**
* Saves the root tag.
*
* @param out The XML writer to use.
* @param includeUniqueID Whether the {@link UniqueID} should be included in the attribute list.
* @param onlyIfNotEmpty Whether to write something even if the file contents are empty.
*/
public void save(XMLWriter out, boolean includeUniqueID, boolean onlyIfNotEmpty) {
if (!onlyIfNotEmpty || !isEmpty()) {
out.startTag(getXMLTagName());
if (includeUniqueID) {
out.writeAttribute(ATTRIBUTE_ID, mId.toString());
}
out.writeAttribute(LoadState.ATTRIBUTE_VERSION, getXMLTagVersion());
out.finishTagEOL();
saveSelf(out);
out.endTagEOL(getXMLTagName(), true);
}
}
/**
* Called to save the data file.
*
* @param out The XML writer to use.
*/
protected abstract void saveSelf(XMLWriter out);
/** @return Whether the file is empty. By default, returns <code>false</code>. */
@SuppressWarnings("static-method")
public boolean isEmpty() {
return false;
}
/** @return The most recent version of the XML tag this object knows how to load. */
public abstract int getXMLTagVersion();
/** @return The XML root container tag name for this particular file. */
public abstract String getXMLTagName();
/**
* Called to match an XML tag name with the root tag for this data file.
*
* @param name The tag name to check.
* @return Whether it matches the root tag or not.
*/
public boolean matchesRootTag(String name) {
return getXMLTagName().equals(name);
}
/** @return The {@link FileType}. */
public abstract FileType getFileType();
/** @return The icons representing this file, at various resolutions. */
public abstract StdImageSet getFileIcons();
/** @return The file associated with this data file. */
public File getFile() {
return mFile;
}
/** @param file The file associated with this data file. */
public void setFile(File file) {
if (file != null) {
file = PathUtils.getFile(PathUtils.getFullPath(file));
}
mFile = file;
}
/** @return The ID for this data file. */
public UUID getId() {
return mId;
}
/** Replaces the existing ID with a new randomly generated one. */
public void generateNewId() {
mId = UUID.randomUUID();
}
/** @return <code>true</code> if the data has been modified. */
public final boolean isModified() {
return mModified;
}
/** @param modified Whether or not the data has been modified. */
public final void setModified(boolean modified) {
if (mModified != modified) {
mModified = modified;
for (DataModifiedListener listener : mDataModifiedListeners.toArray(new DataModifiedListener[mDataModifiedListeners.size()])) {
listener.dataModificationStateChanged(this, mModified);
}
}
}
/** @param listener The listener to add. */
public void addDataModifiedListener(DataModifiedListener listener) {
mDataModifiedListeners.remove(listener);
mDataModifiedListeners.add(listener);
}
/** @param listener The listener to remove. */
public void removeDataModifiedListener(DataModifiedListener listener) {
mDataModifiedListeners.remove(listener);
}
/** Resets the underlying {@link Notifier} by removing all targets. */
public void resetNotifier() {
mNotifier.reset();
}
/**
* Resets the underlying {@link Notifier} by removing all targets except the specified ones.
*
* @param exclude The {@link NotifierTarget}(s) to exclude.
*/
public void resetNotifier(NotifierTarget... exclude) {
mNotifier.reset(exclude);
}
/**
* Registers a {@link NotifierTarget} with this data file's {@link Notifier}.
*
* @param target The {@link NotifierTarget} to register.
* @param names The names to register for.
*/
public void addTarget(NotifierTarget target, String... names) {
mNotifier.add(target, names);
}
/**
* Un-registers a {@link NotifierTarget} with this data file's {@link Notifier}.
*
* @param target The {@link NotifierTarget} to un-register.
*/
public void removeTarget(NotifierTarget target) {
mNotifier.remove(target);
}
/**
* Starts the notification process. Should be called before calling
* {@link #notify(String,Object)}.
*/
public void startNotify() {
if (mNotifier.getBatchLevel() == 0) {
startNotifyAtBatchLevelZero();
}
mNotifier.startBatch();
}
/**
* Called when {@link #startNotify()} is called and the current batch level is zero.
*/
protected void startNotifyAtBatchLevelZero() {
// Does nothing by default.
}
/**
* Sends a notification to all interested consumers.
*
* @param type The notification type.
* @param data Extra data specific to this notification.
*/
public void notify(String type, Object data) {
setModified(true);
mNotifier.notify(this, type, data);
notifyOccured();
}
/** Called when {@link #notify(String,Object)} is called. */
protected void notifyOccured() {
// Does nothing by default.
}
/**
* Ends the notification process. Must be called after calling {@link #notify(String,Object)}.
*/
public void endNotify() {
if (mNotifier.getBatchLevel() == 1) {
endNotifyAtBatchLevelOne();
}
mNotifier.endBatch();
}
/**
* Called when {@link #endNotify()} is called and the current batch level is one.
*/
protected void endNotifyAtBatchLevelOne() {
// Does nothing by default.
}
/**
* Sends a notification to all interested consumers.
*
* @param type The notification type.
* @param data Extra data specific to this notification.
*/
public void notifySingle(String type, Object data) {
startNotify();
notify(type, data);
endNotify();
}
/** @return The {@link StdUndoManager} to use. */
@Override
public final StdUndoManager getUndoManager() {
return mUndoManager;
}
/** @param mgr The {@link StdUndoManager} to use. */
public final void setUndoManager(StdUndoManager mgr) {
mUndoManager = mgr;
}
/** @param edit The {@link UndoableEdit} to add. */
public final void addEdit(UndoableEdit edit) {
mUndoManager.addEdit(edit);
}
/**
* @return <code>true</code> if sorting a list should be considered a change that marks the file
* dirty.
*/
public final boolean sortingMarksDirty() {
return mSortingMarksDirty;
}
/**
* @param markDirty <code>true</code> if sorting a list should be considered a change that marks
* the file dirty.
*/
public final void setSortingMarksDirty(boolean markDirty) {
mSortingMarksDirty = markDirty;
}
}