/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.core.internal.dom;
import static org.xmind.core.internal.dom.DOMConstants.ATTR_ID;
import static org.xmind.core.internal.dom.DOMConstants.TAG_SHEET;
import static org.xmind.core.internal.zip.ArchiveConstants.CONTENT_XML;
import static org.xmind.core.internal.zip.ArchiveConstants.MANIFEST_XML;
import static org.xmind.core.internal.zip.ArchiveConstants.META_XML;
import static org.xmind.core.internal.zip.ArchiveConstants.PATH_MARKER_SHEET;
import static org.xmind.core.internal.zip.ArchiveConstants.PATH_REVISIONS;
import static org.xmind.core.internal.zip.ArchiveConstants.REVISIONS_XML;
import static org.xmind.core.internal.zip.ArchiveConstants.STYLES_XML;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.zip.ZipOutputStream;
import org.w3c.dom.Element;
import org.xmind.core.Core;
import org.xmind.core.CoreException;
import org.xmind.core.IAdaptable;
import org.xmind.core.IChecksumStream;
import org.xmind.core.IEncryptionData;
import org.xmind.core.IFileEntry;
import org.xmind.core.IManifest;
import org.xmind.core.IRevision;
import org.xmind.core.IRevisionManager;
import org.xmind.core.internal.security.Crypto;
import org.xmind.core.internal.zip.ArchiveConstants;
import org.xmind.core.internal.zip.ZipStreamOutputTarget;
import org.xmind.core.io.DirectoryOutputTarget;
import org.xmind.core.io.ICloseableOutputTarget;
import org.xmind.core.io.IInputSource;
import org.xmind.core.io.IOutputTarget;
import org.xmind.core.marker.IMarkerSheet;
import org.xmind.core.style.IStyleSheet;
import org.xmind.core.util.DOMUtils;
import org.xmind.core.util.FileUtils;
/**
* @author frankshaka
*
*/
public class WorkbookSaver {
/**
* The workbook to save
*/
private WorkbookImpl workbook;
/**
* The target to save to
*/
private IOutputTarget target;
/**
* (Optional) The absolute path representing a ZIP file target
*/
private String file;
/**
* Saved entry paths for one 'save' process
*/
private Set<String> savedEntries;
/**
* Whether to skip revisions when saving.
*/
private boolean skipRevisions = false;
/**
* @param workbook
* @param file
*/
public WorkbookSaver(WorkbookImpl workbook, String file) {
super();
this.workbook = workbook;
this.file = file;
}
/**
* @return the file path
*/
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
/**
* @param skipRevisions
* the skipRevisions to set
*/
public void setSkipRevisions(boolean skipRevisions) {
this.skipRevisions = skipRevisions;
}
public boolean isSkipRevisions() {
return skipRevisions;
}
/**
*
* @throws IOException
* @throws CoreException
*/
public void save() throws IOException, CoreException {
save(this.target, this.file);
}
/**
*
* @param output
* @throws IOException
* @throws CoreException
*/
public void save(OutputStream output) throws IOException, CoreException {
save(new ZipStreamOutputTarget(new ZipOutputStream(output)), null);
// the target can't be reused
this.target = null;
}
public void save(String file) throws IOException, CoreException {
save(null, file);
// the target can't be reused
this.target = null;
}
public void save(IOutputTarget target) throws IOException, CoreException {
save(target, null);
}
/**
*
* @param target
* @param file
* @throws IOException
* @throws CoreException
*/
private void save(IOutputTarget target, String file) throws IOException,
CoreException {
if (target == null) {
if (file != null) {
if (new File(file).isDirectory()) {
target = new DirectoryOutputTarget(file);
} else {
target = new ZipStreamOutputTarget(new ZipOutputStream(
new FileOutputStream(file)));
}
}
}
this.file = file;
if (target == null)
throw new FileNotFoundException("No target to save."); //$NON-NLS-1$
try {
doSave(target);
} finally {
if (target instanceof ICloseableOutputTarget) {
((ICloseableOutputTarget) target).close();
}
}
}
private void doSave(IOutputTarget target) throws FileNotFoundException,
IOException, CoreException {
this.target = target;
this.savedEntries = null;
try {
doSave();
} finally {
try {
clearEncryptionData();
} catch (Throwable ignore) {
}
this.savedEntries = null;
}
}
/**
* The main saving process.
*
* @throws IOException
* @throws CoreException
*/
private void doSave() throws IOException, CoreException {
saveMeta();
saveContent();
saveMarkerSheet();
saveStyleSheet();
if (!skipRevisions) {
saveRevisions();
}
copyOtherStaff();
saveManifest();
}
private void saveManifest() throws IOException, CoreException {
saveDOM(workbook.getManifest(), target, MANIFEST_XML);
}
private void saveStyleSheet() throws IOException, CoreException {
IStyleSheet styleSheet = workbook.getStyleSheet();
if (!styleSheet.isEmpty()) {
saveDOM(styleSheet, target, STYLES_XML);
}
}
private void saveMarkerSheet() throws IOException, CoreException {
IMarkerSheet markerSheet = workbook.getMarkerSheet();
if (!markerSheet.isEmpty()) {
saveDOM(markerSheet, target, PATH_MARKER_SHEET);
}
}
private void saveRevisions() throws IOException, CoreException {
Iterator<Element> it = DOMUtils.childElementIterByTag(
workbook.getWorkbookElement(), TAG_SHEET);
while (it.hasNext()) {
Element sheetEle = it.next();
String sheetId = sheetEle.getAttribute(ATTR_ID);
IRevisionManager manager = workbook.getRevisionRepository()
.getRevisionManager(sheetId, IRevision.SHEET);
String path = PATH_REVISIONS + sheetId + "/" + REVISIONS_XML; //$NON-NLS-1$
saveDOM(manager, target, path);
}
}
private void saveContent() throws IOException, CoreException {
saveDOM(workbook, target, CONTENT_XML);
}
private void saveMeta() throws IOException, CoreException {
saveDOM(workbook.getMeta(), target, META_XML);
}
private void copyOtherStaff() throws IOException, CoreException {
IInputSource source = workbook.getTempStorage().getInputSource();
copyAll(source, target);
}
private void copyAll(IInputSource source, IOutputTarget target) {
IManifest manifest = workbook.getManifest();
for (IFileEntry entry : manifest.getFileEntries()) {
if (!entry.isDirectory()) {
String entryPath = entry.getPath();
if (shouldSaveEntry(entryPath)) {
copyEntry(source, target, entryPath);
markSaved(entryPath);
}
}
}
}
private boolean shouldSaveEntry(String entryPath) {
return entryPath != null
&& !"".equals(entryPath) //$NON-NLS-1$
&& !ArchiveConstants.MANIFEST_XML.equals(entryPath)
&& !hasBeenSaved(entryPath)
&& (!skipRevisions || !entryPath
.startsWith(ArchiveConstants.PATH_REVISIONS));
}
private void copyEntry(IInputSource source, IOutputTarget target,
String entryPath) {
try {
InputStream in = getInputStream(source, entryPath);
if (in != null) {
OutputStream out = getOutputStream(target, entryPath);
if (out != null) {
try {
FileUtils.transfer(in, out, true);
} finally {
long time = source.getEntryTime(entryPath);
if (time >= 0) {
target.setEntryTime(entryPath, time);
}
recordChecksum(entryPath, out);
}
}
}
} catch (IOException e) {
Core.getLogger().log(e);
} catch (CoreException e) {
Core.getLogger().log(e);
}
}
private InputStream getInputStream(IInputSource source, String entryPath) {
if (source.hasEntry(entryPath)) {
return source.getEntryStream(entryPath);
}
return null;
}
/**
* @param manifest
*/
private void clearEncryptionData() {
for (IFileEntry entry : workbook.getManifest().getFileEntries()) {
entry.deleteEncryptionData();
}
}
private boolean hasBeenSaved(String entryPath) {
return savedEntries != null && savedEntries.contains(entryPath);
}
/**
* @param entryPath
*/
private void markSaved(String entryPath) {
if (savedEntries == null)
savedEntries = new HashSet<String>();
savedEntries.add(entryPath);
}
/**
* @param domAdapter
* @param target
* @param entryPath
*/
private void saveDOM(IAdaptable domAdapter, IOutputTarget target,
String entryPath) throws IOException, CoreException {
OutputStream out = getOutputStream(target, entryPath);
if (out != null) {
try {
DOMUtils.save(domAdapter, out, true);
} finally {
recordChecksum(entryPath, out);
markSaved(entryPath);
}
}
}
/**
* @param entryPath
* @param out
* @throws IOException
*/
private void recordChecksum(String entryPath, Object checksumProvider)
throws IOException {
if (checksumProvider instanceof IChecksumStream) {
IEncryptionData encData = workbook.getManifest().getEncryptionData(
entryPath);
if (encData != null && encData.getChecksumType() != null) {
String checksum = ((IChecksumStream) checksumProvider)
.getChecksum();
if (checksum != null) {
encData.setAttribute(checksum, DOMConstants.ATTR_CHECKSUM);
}
}
}
}
private OutputStream getOutputStream(IOutputTarget target, String entryPath)
throws CoreException {
if (!target.isEntryAvaialble(entryPath))
return null;
OutputStream out = target.getEntryStream(entryPath);
if (out == null)
return null;
String password = workbook.getPassword();
if (password == null)
return out;
IFileEntry entry = workbook.getManifest().getFileEntry(entryPath);
if (entry == null)
return out;
if (ignoresEncryption(entry, entryPath))
return out;
IEncryptionData encData = entry.createEncryptionData();
return Crypto.creatOutputStream(out, true, encData, password);
}
private boolean ignoresEncryption(IFileEntry entry, String entryPath) {
return ArchiveConstants.MANIFEST_XML.equals(entryPath)
|| ((FileEntryImpl) entry).isIgnoreEncryption();
}
}