/* ****************************************************************************** * Copyright (c) 2006-2016 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.zip.ArchiveConstants.COMMENTS_XML; 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_EXTENSIONS; 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.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.zip.ZipOutputStream; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xmind.core.Core; import org.xmind.core.CoreException; import org.xmind.core.IAdaptable; import org.xmind.core.ICommentManager; import org.xmind.core.IEntryStreamNormalizer; import org.xmind.core.IFileEntry; import org.xmind.core.IMeta; import org.xmind.core.IRevisionManager; import org.xmind.core.IRevisionRepository; import org.xmind.core.ISerializer; import org.xmind.core.IWorkbook; import org.xmind.core.IWorkbookExtension; import org.xmind.core.IWorkbookExtensionManager; import org.xmind.core.internal.AbstractSerializingBase; import org.xmind.core.internal.zip.ArchiveConstants; import org.xmind.core.internal.zip.ZipStreamOutputTarget; import org.xmind.core.io.ByteArrayStorage; import org.xmind.core.io.IInputSource; import org.xmind.core.io.IOutputTarget; import org.xmind.core.io.IStorage; import org.xmind.core.marker.IMarkerSheet; import org.xmind.core.style.IStyleSheet; import org.xmind.core.util.DOMUtils; import org.xmind.core.util.FileUtils; import org.xmind.core.util.IProgressReporter; /** * * @author Frank Shaka * @since 3.6.50 */ public class SerializerImpl extends AbstractSerializingBase implements ISerializer { private IWorkbook workbook; private IOutputTarget outputTarget; private final Set<String> encryptionIgnoredEntries; private String[] preferredEncryptionIgnoredEntries; private ZipOutputStream intermediateOutputStream; private boolean compressed; private boolean usesWorkbookStorageAsOutputTarget; private ManifestImpl manifest; private ManifestImpl tempManifest; private final Set<String> serializedEntryPaths; public SerializerImpl() { super(); this.workbook = null; this.outputTarget = null; this.encryptionIgnoredEntries = new HashSet<String>(); this.preferredEncryptionIgnoredEntries = null; this.intermediateOutputStream = null; this.compressed = false; this.usesWorkbookStorageAsOutputTarget = false; this.manifest = null; this.tempManifest = null; this.serializedEntryPaths = new HashSet<String>(); } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#getWorkbook() */ public IWorkbook getWorkbook() { return workbook; } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#setWorkbook(org.xmind.core.IWorkbook) */ public void setWorkbook(IWorkbook workbook) { if (workbook == null) throw new IllegalArgumentException("Workbook is null"); //$NON-NLS-1$ if (!(workbook instanceof WorkbookImpl)) throw new IllegalArgumentException("Can't serialize this workbook"); //$NON-NLS-1$ IStorage storage = null; if (usesWorkbookStorageAsOutputTarget) { storage = workbook.getAdapter(IStorage.class); if (storage == null) throw new IllegalArgumentException( "No workbook storage available"); //$NON-NLS-1$ } this.workbook = workbook; if (storage != null) { doSetOutputTarget(storage.getOutputTarget()); } } protected IOutputTarget getOutputTarget() { return this.outputTarget; } protected void doSetOutputTarget(IOutputTarget target) { this.outputTarget = target; } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#hasOutputTarget() */ public boolean hasOutputTarget() { return outputTarget != null; } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#setOutputTarget(org.xmind.core.io. * IOutputTarget) */ public void setOutputTarget(IOutputTarget target) { if (target == null) throw new IllegalArgumentException("output target is null"); //$NON-NLS-1$ doSetOutputTarget(target); this.intermediateOutputStream = null; this.usesWorkbookStorageAsOutputTarget = false; } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#setOutputStream(java.io.OutputStream) */ public void setOutputStream(OutputStream stream) { if (stream == null) throw new IllegalArgumentException("stream is null"); //$NON-NLS-1$ this.intermediateOutputStream = new ZipOutputStream(stream); doSetOutputTarget(new ZipStreamOutputTarget(intermediateOutputStream, compressed)); this.usesWorkbookStorageAsOutputTarget = false; } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#setWorkbookStorageAsOutputTarget() */ public void setWorkbookStorageAsOutputTarget() { if (getWorkbook() != null) { IStorage storage = getWorkbook().getAdapter(IStorage.class); if (storage == null) throw new IllegalArgumentException( "no workbook storage available"); //$NON-NLS-1$ doSetOutputTarget(storage.getOutputTarget()); } else { /// sets a fake output target that will be substituted when /// workbook is set doSetOutputTarget(new ByteArrayStorage().getOutputTarget()); } this.usesWorkbookStorageAsOutputTarget = true; this.intermediateOutputStream = null; } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#getEncryptionIgnoredEntries() */ public String[] getEncryptionIgnoredEntries() { return preferredEncryptionIgnoredEntries; } /* * (non-Javadoc) * * @see * org.xmind.core.ISerializer#setEncryptionIgnoredEntries(java.lang.String[] * ) */ public void setEncryptionIgnoredEntries(String[] entryPaths) { this.preferredEncryptionIgnoredEntries = entryPaths; encryptionIgnoredEntries.clear(); collectDefaultEncryptionIgnoredEntries(encryptionIgnoredEntries); if (entryPaths != null) { encryptionIgnoredEntries.addAll(Arrays.asList(entryPaths)); } } protected boolean isEntryEncryptionIgnored(String entryPath) { return encryptionIgnoredEntries.contains(entryPath); } protected void collectDefaultEncryptionIgnoredEntries( Set<String> entryPaths) { entryPaths.add(ArchiveConstants.MANIFEST_XML); } /* * (non-Javadoc) * * @see org.xmind.core.ISerializer#serialize(org.xmind.core.util. * IProgressReporter) */ public void serialize(IProgressReporter reporter) throws IOException, CoreException, IllegalStateException { WorkbookImpl workbook = (WorkbookImpl) getWorkbook(); if (workbook == null) throw new IllegalStateException("no workbook to serialize"); //$NON-NLS-1$ if (!hasOutputTarget()) throw new IllegalStateException("no output target specified"); //$NON-NLS-1$ manifest = workbook.getManifest(); IEntryStreamNormalizer oldNormalizer = manifest.getStreamNormalizer(); IEntryStreamNormalizer newNormalizer = getEntryStreamNormalizer(); boolean normalizerChanged = newNormalizer != null && !newNormalizer.equals(oldNormalizer); if (usesWorkbookStorageAsOutputTarget) { tempManifest = manifest; if (normalizerChanged) { /// use new normalizer to save XML files tempManifest.setStreamNormalizer(newNormalizer); } } else { Document tempImplementation = cloneDocument( manifest.getImplementation(), ArchiveConstants.MANIFEST_XML); tempManifest = new ManifestImpl(tempImplementation, new WriteOnlyStorage(getOutputTarget())); if (newNormalizer != null) { tempManifest.setStreamNormalizer(newNormalizer); } else { tempManifest.setStreamNormalizer(oldNormalizer); } /// Give this manifest a temp owner workbook to prevent null /// pointer exception when file entry events are triggered. new WorkbookImpl(DOMUtils.createDocument(), tempManifest); } /// save meta.xml IMeta meta = workbook.getMeta(); String creatorName = getCreatorName(); if (creatorName != null) meta.setValue(IMeta.CREATOR_NAME, creatorName); String creatorVersion = getCreatorVersion(); if (creatorVersion != null) meta.setValue(IMeta.CREATOR_VERSION, creatorVersion); serializeXML(meta, META_XML); /// save content.xml serializeXML(workbook, CONTENT_XML); /// NOTE: XML files should always serialized when saving to the /// workbook's temp storage, otherwise recovered workbooks may contain /// invalid data. /// save markers/markerSheet.xml IMarkerSheet markerSheet = workbook.getMarkerSheet(); if (usesWorkbookStorageAsOutputTarget || !markerSheet.isEmpty()) { serializeXML(markerSheet, PATH_MARKER_SHEET); } else { tempManifest.deleteFileEntry(PATH_MARKER_SHEET); serializedEntryPaths.add(PATH_MARKER_SHEET); } /// save styles.xml IStyleSheet styleSheet = workbook.getStyleSheet(); if (usesWorkbookStorageAsOutputTarget || !styleSheet.isEmpty()) { serializeXML(styleSheet, STYLES_XML); } else { tempManifest.deleteFileEntry(STYLES_XML); serializedEntryPaths.add(STYLES_XML); } /// save comments.xml ICommentManager commentManager = workbook.getCommentManager(); if (usesWorkbookStorageAsOutputTarget || !commentManager.isEmpty()) { serializeXML(commentManager, COMMENTS_XML); } else { tempManifest.deleteFileEntry(COMMENTS_XML); serializedEntryPaths.add(COMMENTS_XML); } /// save extensions IWorkbookExtensionManager extensionManager = ((IWorkbook) workbook) .getAdapter(IWorkbookExtensionManager.class); List<IWorkbookExtension> exts = extensionManager.getExtensions(); for (IWorkbookExtension ext : exts) { String providerName = ext.getProviderName(); String path = PATH_EXTENSIONS + providerName + ".xml"; //$NON-NLS-1$ serializeXML(ext, path); } /// save revisions IRevisionRepository revisionRepository = workbook .getRevisionRepository(); for (String resourceId : revisionRepository .getRegisteredResourceIds()) { IRevisionManager manager = revisionRepository .getRegisteredRevisionManager(resourceId); if (manager != null) { String path = PATH_REVISIONS + resourceId + "/" //$NON-NLS-1$ + REVISIONS_XML; serializeXML(manager, path); } } /// copy remaining file entries, e.g. attachments, etc. Iterator<IFileEntry> sourceEntryIter; if (!usesWorkbookStorageAsOutputTarget) { /// saving to external location, /// write only referenced file entries sourceEntryIter = manifest.iterFileEntries(); } else if (normalizerChanged) { /// saving to internal storage when encryption is changed, /// re-encrypt all file entries sourceEntryIter = manifest.getAllRegisteredEntries().iterator(); } else { /// saving to internal storage when encryption is not changed, /// touch no file entries sourceEntryIter = null; } while (sourceEntryIter != null && sourceEntryIter.hasNext()) { IFileEntry sourceEntry = sourceEntryIter.next(); if (sourceEntry.isDirectory() || !sourceEntry.canRead()) continue; String entryPath = sourceEntry.getPath(); if (MANIFEST_XML.equals(entryPath) || serializedEntryPaths.contains(entryPath)) continue; IFileEntry targetEntry = tempManifest.getFileEntry(entryPath); if (targetEntry == null) // TODO missing entry, need log? continue; if (usesWorkbookStorageAsOutputTarget) { /// saving to internal storage, /// write to a temporary entry first to protect original entry String tempEntryPath = makeTempPath(entryPath); /// make sure we use the old normalizer to decrypt the file entry manifest.setStreamNormalizer(oldNormalizer); InputStream entryInput = sourceEntry.openInputStream(); try { OutputStream tempOutput = tempManifest.getStorage() .getOutputTarget().openEntryStream(tempEntryPath); try { FileUtils.transfer(entryInput, tempOutput, false); } finally { tempOutput.close(); } } finally { entryInput.close(); } /// make sure we use the new normalizer to encrypt the file entry tempManifest.setStreamNormalizer(newNormalizer); InputStream tempInput = tempManifest.getStorage() .getInputSource().openEntryStream(tempEntryPath); try { OutputStream entryOutput = openEntryOutputStream(entryPath); try { FileUtils.transfer(tempInput, entryOutput, false); } finally { entryOutput.close(); } } finally { tempInput.close(); } } else { /// saving to external location, /// just copy the entry directly InputStream entryInput = sourceEntry.openInputStream(); try { OutputStream entryOutput = openEntryOutputStream(entryPath); try { FileUtils.transfer(entryInput, entryOutput, false); } finally { entryOutput.close(); } } finally { entryInput.close(); } } } if (usesWorkbookStorageAsOutputTarget && normalizerChanged) { /// keep the new normalizer in the original manifest /// to decrypt data in the internal storage afterwards manifest.setStreamNormalizer(newNormalizer); } /// save manifest.xml serializeXML(tempManifest, MANIFEST_XML); /// only upon success should we close zip stream if (intermediateOutputStream != null) { intermediateOutputStream.finish(); intermediateOutputStream.flush(); intermediateOutputStream.close(); } } private static Document cloneDocument(Document document, String xmlName) throws CoreException { try { Transformer transformer = createXMLSerializer(); DOMResult result = new DOMResult(); transformer.transform(new DOMSource(document), result); return (Document) result.getNode(); } catch (TransformerException e) { throw new CoreException(Core.ERROR_FAIL_SERIALIZING_XML, xmlName, e); } } private static Transformer createXMLSerializer() throws CoreException { /// create a new transformer instance each time return DOMUtils.getDefaultTransformer(); } private String makeTempPath(String path) { int sepIndex = path.lastIndexOf('/'); if (sepIndex >= 0) { return path.substring(0, sepIndex + 1) + "._." //$NON-NLS-1$ + path.substring(sepIndex + 1); } // no separator return "._." + path; //$NON-NLS-1$ } private void serializeXML(IAdaptable domAdaptable, String entryPath) throws IOException, CoreException { Node node = (Node) domAdaptable.getAdapter(Node.class); if (node == null) throw new CoreException(Core.ERROR_INVALID_ARGUMENT, "Object has no DOM node"); //$NON-NLS-1$ Transformer transformer = createXMLSerializer(); OutputStream out = openEntryOutputStream(entryPath); try { transformer.transform(new DOMSource(node), new StreamResult(out)); } catch (TransformerException e) { if (e.getCause() != null && e.getCause() instanceof IOException) { throw (IOException) e.getCause(); } throw new CoreException(Core.ERROR_FAIL_SERIALIZING_XML, entryPath, e); } finally { out.close(); } serializedEntryPaths.add(entryPath); } private OutputStream openEntryOutputStream(String entryPath) throws IOException, CoreException { IFileEntry entry = tempManifest.getFileEntry(entryPath); if (isEntryEncryptionIgnored(entryPath)) { entry.deleteEncryptionData(); return tempManifest.getStorage().getOutputTarget() .openEntryStream(entryPath); } if (entry == null) { entry = tempManifest.createFileEntry(entryPath); entry.increaseReference(); } return entry.openOutputStream(); } // String calcChecksum(Object checksumProvider) { // if (checksumProvider instanceof IChecksumStream) { // return ((IChecksumStream) checksumProvider).getChecksum(); // } // return null; // } // void recordChecksum(String entryPath, Object checksumProvider) { // String checksum = calcChecksum(checksumProvider); // if (checksum == null) // return; // // IFileEntry entry = tempManifest.getFileEntry(entryPath); // if (entry == null) // return; // // IEncryptionData encData = entry.getEncryptionData(); // if (encData == null || encData.getChecksumType() == null) // return; // // encData.setAttribute(checksum, DOMConstants.ATTR_CHECKSUM); // } private static class WriteOnlyStorage implements IStorage { private IOutputTarget target; public WriteOnlyStorage(IOutputTarget target) { this.target = target; } public IInputSource getInputSource() { throw new UnsupportedOperationException(); } public IOutputTarget getOutputTarget() { return target; } public String getName() { return toString(); } public String getFullPath() { return getName(); } public void clear() { } public void deleteEntry(String entryName) { } public void renameEntry(String entryName, String newName) { } } }