Index: src/test/java/com/xpn/xwiki/plugin/zipexplorer/ZipExplorerTest.java =================================================================== --- src/test/java/com/xpn/xwiki/plugin/zipexplorer/ZipExplorerTest.java (revision 27944) +++ src/test/java/com/xpn/xwiki/plugin/zipexplorer/ZipExplorerTest.java (working copy) @@ -110,6 +110,7 @@ assertEquals("Directory/File.txt", newAttachment.getFilename()); assertEquals(zipFileContent.length(), newAttachment.getFilesize()); + assertEquals(zipFileContent.length(), newAttachment.getContentSize(context)); assertEquals(zipFileContent, new String(newAttachment.getContent(context))); } @@ -219,7 +220,11 @@ mockAttachment.stubs().method("getDoc").will(returnValue(document)); mockAttachment.stubs().method("getAuthor").will(returnValue("Vincent")); mockAttachment.stubs().method("getDate").will(returnValue(new Date())); - mockAttachment.stubs().method("getContent").will(returnValue(content)); + mockAttachment.stubs().method("getFilesize").will(returnValue((content == null) ? 0 : content.length)); + mockAttachment.stubs().method("getContentSize").will(returnValue((content == null) ? 0 : content.length)); + mockAttachment.stubs().method("getContent").will(returnValue((content == null) ? new byte[0] : content)); + mockAttachment.stubs().method("getContentInputStream").will( + returnValue(new ByteArrayInputStream((content == null) ? new byte[0] : content))); return (XWikiAttachment) mockAttachment.proxy(); } Index: src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java =================================================================== --- src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java (revision 27944) +++ src/test/java/com/xpn/xwiki/doc/XWikiDocumentArchiveTest.java (working copy) @@ -21,8 +21,11 @@ import java.util.Date; +import org.jmock.Mock; import org.suigeneris.jrcs.rcs.Version; +import com.xpn.xwiki.XWiki; +import com.xpn.xwiki.XWikiConfig; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.test.AbstractBridgedXWikiComponentTestCase; @@ -36,10 +39,19 @@ public class XWikiDocumentArchiveTest extends AbstractBridgedXWikiComponentTestCase { private XWikiContext context; + + private Mock mockXWiki; + protected void setUp() throws Exception { super.setUp(); + + this.mockXWiki = mock(XWiki.class); + this.mockXWiki.stubs().method("getEncoding").will(returnValue("iso-8859-1")); + this.mockXWiki.stubs().method("getConfig").will(returnValue(new XWikiConfig())); + this.context = new XWikiContext(); + this.context.setWiki((XWiki) this.mockXWiki.proxy()); } /** Index: src/main/java/com/xpn/xwiki/plugin/zipexplorer/ZipExplorerPlugin.java =================================================================== --- src/main/java/com/xpn/xwiki/plugin/zipexplorer/ZipExplorerPlugin.java (revision 27944) +++ src/main/java/com/xpn/xwiki/plugin/zipexplorer/ZipExplorerPlugin.java (working copy) @@ -22,6 +22,7 @@ import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; @@ -129,13 +130,11 @@ newAttachment.setDate(attachment.getDate()); try { - byte[] stream = attachment.getContent(context); - ByteArrayInputStream bais = new ByteArrayInputStream(stream); - if (!isZipFile(bais)) { + if (!isZipFile(attachment.getContentInputStream(context))) { return attachment; } - ZipInputStream zis = new ZipInputStream(bais); + ZipInputStream zis = new ZipInputStream(attachment.getContentInputStream(context)); ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { @@ -144,13 +143,16 @@ if (entryName.equals(filename)) { newAttachment.setFilename(entryName); - // Note: We're copying the content of the file in the ZIP in memory. This is - // potentially going to cause an error if the file's size is greater than the - // maximum size of a byte[] array in Java or if there's not enough memomry. - byte[] data = IOUtils.toByteArray(zis); + if (entry.getSize() == -1) { + // Note: We're copying the content of the file in the ZIP in memory. This is + // potentially going to cause an error if the file's size is greater than the + // maximum size of a byte[] array in Java or if there's not enough memomry. + byte[] data = IOUtils.toByteArray(zis); - newAttachment.setFilesize(data.length); - newAttachment.setContent(data); + newAttachment.setContent(data); + } else { + newAttachment.setContent(zis, (int) entry.getSize()); + } break; } } @@ -292,7 +294,7 @@ * @param filecontent the content of the file * @return true if the file is in zip format (.zip, .jar etc) or false otherwise */ - protected boolean isZipFile(ByteArrayInputStream filecontent) + protected boolean isZipFile(InputStream filecontent) { int standardZipHeader = 0x504b0304; try { @@ -304,8 +306,12 @@ } catch (IOException e) { // The file doesn't have 4 bytes, so it isn't a zip file } finally { - // Reset the input stream to the beginning. This is needed for further reading the archive. - filecontent.reset(); + // Reset the input stream to the beginning. This may be needed for further reading the archive. + try { + filecontent.reset(); + } catch (IOException e) { + e.printStackTrace(); + } } return false; } Index: src/main/java/com/xpn/xwiki/plugin/fileupload/FileUploadPlugin.java =================================================================== --- src/main/java/com/xpn/xwiki/plugin/fileupload/FileUploadPlugin.java (revision 27944) +++ src/main/java/com/xpn/xwiki/plugin/fileupload/FileUploadPlugin.java (working copy) @@ -282,15 +282,16 @@ */ public byte[] getFileItemData(String formfieldName, XWikiContext context) throws XWikiException { - FileItem fileitem = getFile(formfieldName, context); + int size = getFileItemSize(formfieldName, context); - if (fileitem == null) { + if (size == 0) { return null; } - byte[] data = new byte[(int) fileitem.getSize()]; + byte[] data = new byte[size]; + try { - InputStream fileis = fileitem.getInputStream(); + InputStream fileis = getFileItemInputStream(formfieldName, context); if (fileis != null) { fileis.read(data); fileis.close(); @@ -308,6 +309,48 @@ } /** + * Allows to retrieve the contents of an uploaded file as a stream. {@link #loadFileList(XWikiContext)} needs to be + * called beforehand. + * + * @param formfieldName The name of the form field. + * @param context Context of the request. + * @return a InputStream on the file content + * @throws IOException if I/O problem occurs + * + * @since 2.3M2 + */ + public InputStream getFileItemInputStream(String formfieldName, XWikiContext context) throws IOException + { + FileItem fileitem = getFile(formfieldName, context); + + if (fileitem == null) { + return null; + } + + return fileitem.getInputStream(); + } + + /** + * Retrieve the size of a file content in byte. {@link #loadFileList(XWikiContext)} needs to be called beforehand. + * + * @param formfieldName The name of the form field. + * @param context Context of the request. + * @return the size of the file in byte + * + * @since 2.3M2 + */ + public int getFileItemSize(String formfieldName, XWikiContext context) + { + FileItem fileitem = getFile(formfieldName, context); + + if (fileitem == null) { + return 0; + } + + return ((int) fileitem.getSize()); + } + + /** * Allows to retrieve the contents of an uploaded file as a string. {@link #loadFileList(XWikiContext)} needs to be * called beforehand. * Index: src/main/java/com/xpn/xwiki/plugin/packaging/PackageAPI.java =================================================================== --- src/main/java/com/xpn/xwiki/plugin/packaging/PackageAPI.java (revision 27944) +++ src/main/java/com/xpn/xwiki/plugin/packaging/PackageAPI.java (working copy) @@ -22,6 +22,7 @@ package com.xpn.xwiki.plugin.packaging; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -240,11 +241,39 @@ } } - public String Import(byte file[]) throws IOException, XWikiException + /** + * Load a package in memory from a byte array. It may be installed later using {@link #install()}. + * Your should prefer {@link #Import(InputStream, XWikiContext) which may avoid loading the package twice in memory. + * + * @param file an byte array containing a zipped package file + * + * @return an empty string, useless. + * + * @throws IOException while reading the ZipFile + * @throws XWikiException when package content is broken + */ + public String Import(byte file[]) throws IOException, XWikiException { return this.plugin.Import(file, getXWikiContext()); } + /** + * Load a package in memory from an InputStream. It may be installed later using {@link #install()}. + * + * @param file an InputStream of a zipped package file + * + * @return an empty string, useless. + * + * @throws IOException while reading the ZipFile + * @throws XWikiException when package content is broken + * + * @since 2.3M2 + */ + public String Import(InputStream file) throws IOException, XWikiException + { + return this.plugin.Import(file, getXWikiContext()); + } + public int testInstall() { return this.plugin.testInstall(false, getXWikiContext()); Index: src/main/java/com/xpn/xwiki/plugin/packaging/Package.java =================================================================== --- src/main/java/com/xpn/xwiki/plugin/packaging/Package.java (revision 27944) +++ src/main/java/com/xpn/xwiki/plugin/packaging/Package.java (working copy) @@ -28,7 +28,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.StringWriter; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -49,7 +48,6 @@ import org.dom4j.dom.DOMElement; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; -import org.dom4j.io.XMLWriter; import org.xwiki.query.QueryException; import com.xpn.xwiki.XWiki; @@ -58,6 +56,8 @@ import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.classes.BaseClass; +import com.xpn.xwiki.util.UnclosableInputStream; +import com.xpn.xwiki.util.XMLWriter; public class Package { @@ -358,11 +358,39 @@ return ""; } - + /** + * Load this package in memory from a byte array. It may be installed later using {@link #install(XWikiContext)}. + * Your should prefer {@link #Import(InputStream, XWikiContext) which may avoid loading the package twice in memory. + * + * @param file a byte array containing the content of a zipped package file + * @param context current XWikiContext + * + * @return an empty string, useless. + * + * @throws IOException while reading the ZipFile + * @throws XWikiException when package content is broken + */ public String Import(byte file[], XWikiContext context) throws IOException, XWikiException { - ByteArrayInputStream bais = new ByteArrayInputStream(file); - ZipInputStream zis = new ZipInputStream(bais); + return Import(new ByteArrayInputStream(file), context); + } + + /** + * Load this package in memory from an InputStream. It may be installed later using {@link #install(XWikiContext)}. + * + * @param file an InputStream of a zipped package file + * @param context current XWikiContext + * + * @return an empty string, useless. + * + * @throws IOException while reading the ZipFile + * @throws XWikiException when package content is broken + * + * @since 2.3M2 + */ + public String Import(InputStream file, XWikiContext context) throws IOException, XWikiException + { + ZipInputStream zis = new ZipInputStream(file); ZipEntry entry; Document description = null; @@ -371,8 +399,8 @@ if (description == null) { throw new PackageException(XWikiException.ERROR_XWIKI_UNKNOWN, "Could not find the package definition"); } - bais = new ByteArrayInputStream(file); - zis = new ZipInputStream(bais); + file.reset(); + zis = new ZipInputStream(file); while ((entry = zis.getNextEntry()) != null) { // Don't read the package.xml file (since we've already read it above), // directories or any file in the META-INF directory (we use that directory @@ -383,7 +411,7 @@ } else { XWikiDocument doc = null; try { - doc = readFromXML(readByteArrayFromInputStream(zis, entry.getSize())); + doc = readFromXML(new UnclosableInputStream(zis)); } catch (Throwable ex) { LOG.warn("Failed to parse document [" + entry.getName() + "] from XML during import, thus it will not be installed. " + "The error was: " @@ -787,18 +815,6 @@ } } - private ByteArrayInputStream readByteArrayFromInputStream(ZipInputStream zin, long size) throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream((size > 0) ? (int) size : 4096); - byte[] data = new byte[4096]; - int cnt; - while ((cnt = zin.read(data, 0, 4096)) != -1) { - baos.write(data, 0, cnt); - } - - return new ByteArrayInputStream(baos.toByteArray()); - } - /** * Create a {@link XWikiDocument} from xml stream. * @@ -846,80 +862,116 @@ return null; } + /** + * You should prefer {@link #toXML(com.xpn.xwiki.util.XMLWriter)}. + * If an error occurs, a stacktrace is dump to logs, and an empty String is returned. + * + * @return a package.xml file for the this package + */ public String toXml(XWikiContext context) { - OutputFormat outputFormat = new OutputFormat("", true); - outputFormat.setEncoding(context.getWiki().getEncoding()); - StringWriter out = new StringWriter(); - XMLWriter writer = new XMLWriter(out, outputFormat); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { - writer.write(toXmlDocument()); - - return out.toString(); + toXML(baos, context); + return baos.toString(context.getWiki().getEncoding()); } catch (IOException e) { e.printStackTrace(); - return ""; } } - private Document toXmlDocument() + /** + * Write the package.xml file to an {@link com.​xpn.​xwiki.​util.XMLWriter} + * + * @param wr the writer to write to + * + * @throws IOException when an error occurs during streaming operation + * + * @since 2.3M2 + */ + public void toXML(XMLWriter wr) throws IOException { - Document doc = new DOMDocument(); Element docel = new DOMElement("package"); - doc.setRootElement(docel); + wr.writeOpen(docel); + Element elInfos = new DOMElement("infos"); - docel.add(elInfos); + wr.write(elInfos); Element el = new DOMElement("name"); el.addText(this.name); - elInfos.add(el); + wr.write(el); el = new DOMElement("description"); el.addText(this.description); - elInfos.add(el); + wr.write(el); el = new DOMElement("licence"); el.addText(this.licence); - elInfos.add(el); + wr.write(el); el = new DOMElement("author"); el.addText(this.authorName); - elInfos.add(el); + wr.write(el); el = new DOMElement("version"); el.addText(this.version); - elInfos.add(el); + wr.write(el); el = new DOMElement("backupPack"); el.addText(new Boolean(this.backupPack).toString()); - elInfos.add(el); + wr.write(el); el = new DOMElement("preserveVersion"); el.addText(new Boolean(this.preserveVersion).toString()); - elInfos.add(el); + wr.write(el); Element elfiles = new DOMElement("files"); - docel.add(elfiles); + wr.writeOpen(elfiles); for (DocumentInfo docInfo : this.files) { Element elfile = new DOMElement("file"); elfile.addAttribute("defaultAction", String.valueOf(docInfo.getAction())); elfile.addAttribute("language", String.valueOf(docInfo.getLanguage())); elfile.addText(docInfo.getFullName()); - elfiles.add(elfile); + wr.write(elfile); } + } - return doc; + /** + * Write the package.xml file to an OutputStream + * + * @param out the OutputStream to write to + * @param context curent XWikiContext + * + * @throws IOException when an error occurs during streaming operation + * + * @since 2.3M2 + */ + public void toXML(OutputStream out, XWikiContext context) throws IOException + { + XMLWriter wr = new XMLWriter(out, new OutputFormat("", true, context.getWiki().getEncoding())); + + Document doc = new DOMDocument(); + wr.writeDocumentStart(doc); + toXML(wr); + wr.writeDocumentEnd(doc); } - private void addInfosToZip(ZipOutputStream zos, XWikiContext context) + /** + * Write the package.xml file to a ZipOutputStream + * + * @param zos the ZipOutputStream to write to + * @param context curent XWikiContext + * + * @throws IOException when an error occurs during streaming operation + */ + private void addInfosToZip(ZipOutputStream zos, XWikiContext context) throws IOException { try { String zipname = DefaultPackageFileName; ZipEntry zipentry = new ZipEntry(zipname); zos.putNextEntry(zipentry); - zos.write(toXml(context).getBytes(context.getWiki().getEncoding())); + toXML(zos, context); zos.closeEntry(); } catch (Exception e) { e.printStackTrace(); @@ -960,30 +1012,21 @@ return fileName.toString(); } + /** + * Write an XML serialized XWikiDocument to a ZipOutputStream + * @param doc the document to serialize + * @param zos the ZipOutputStream to write to + * @param withVersions if true, also serialize all document versions + * @param context current XWikiContext + * + * @throws XWikiException when an error occurs during documents access + * @throws IOException when an error occurs during streaming operation + */ public void addToZip(XWikiDocument doc, ZipOutputStream zos, boolean withVersions, XWikiContext context) - throws IOException + throws XWikiException, IOException { - try { - String zipname = getPathFromDocument(doc, context); - ZipEntry zipentry = new ZipEntry(zipname); - zos.putNextEntry(zipentry); - String docXml = doc.toXML(true, false, true, withVersions, context); - - // Ensure that a non-admin user do not get to see user's passwords. Note that this - // is for backward compatibility for passwords that are stored in clear. As of - // XWiki 1.0 RC2 passwords are now hashed and thus it's no longer important that users - // get to see them. - if (!context.getWiki().getRightService().hasAdminRights(context)) { - docXml = - context.getUtil().substitute("s/.*?<\\/password>/********<\\/password>/goi", - docXml); - } - - zos.write(docXml.getBytes(context.getWiki().getEncoding())); - zos.closeEntry(); - } catch (Exception e) { - e.printStackTrace(); - } + String zipname = getPathFromDocument(doc, context); + doc.addToZip(zos, zipname, withVersions, context); } public void addToDir(XWikiDocument doc, File dir, boolean withVersions, XWikiContext context) throws XWikiException @@ -1001,14 +1044,8 @@ } String filename = getFileNameFromDocument(doc, context); File file = new File(spacedir, filename); - String xml = doc.toXML(true, false, true, withVersions, context); - if (!context.getWiki().getRightService().hasAdminRights(context)) { - xml = - context.getUtil().substitute("s/.*?<\\/password>/********<\\/password>/goi", - xml); - } FileOutputStream fos = new FileOutputStream(file); - fos.write(xml.getBytes(context.getWiki().getEncoding())); + doc.toXML(fos, true, false, true, withVersions, context); fos.flush(); fos.close(); } catch (ExcludeDocumentException e) { @@ -1028,7 +1065,7 @@ String filename = DefaultPackageFileName; File file = new File(dir, filename); FileOutputStream fos = new FileOutputStream(file); - fos.write(toXml(context).getBytes(context.getWiki().getEncoding())); + toXML(fos, context); fos.flush(); fos.close(); } catch (Exception e) { @@ -1065,11 +1102,6 @@ return domdoc; } - protected void readDependencies() - { - - } - public void addAllWikiDocuments(XWikiContext context) throws XWikiException { XWiki wiki = context.getWiki(); Index: src/main/java/com/xpn/xwiki/doc/XWikiAttachmentContent.java =================================================================== --- src/main/java/com/xpn/xwiki/doc/XWikiAttachmentContent.java (revision 27944) +++ src/main/java/com/xpn/xwiki/doc/XWikiAttachmentContent.java (working copy) @@ -21,6 +21,13 @@ package com.xpn.xwiki.doc; +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import org.hibernate.Hibernate; + public class XWikiAttachmentContent implements Cloneable { private XWikiAttachment attachment; @@ -71,6 +78,12 @@ return attachmentcontent; } + /** + * @return a byte array containing the binary content of the attachment + * + * @deprecated use {@link #getContentInputStream()} instead + */ + @Deprecated public byte[] getContent() { if (this.content == null) { @@ -80,6 +93,14 @@ } } + /** + * Set the content from a byte array + * + * @param content a byte array containing the binary data of the attachment + * + * @deprecated use {@link #setContent(java.io.InputStream, int)} instead + */ + @Deprecated public void setContent(byte[] content) { if (content == null) { @@ -112,4 +133,69 @@ { this.isContentDirty = contentDirty; } + + /** + * @return an InputStream to read the binary content of this attachment + * + * @since 2.3M2 + */ + public InputStream getContentInputStream() + { + return new ByteArrayInputStream(getContent()); + } + + /** + * set the content of the attachment from an InputStream + * + * @param is the input stream that will be read + * @param length the length in byte to read + * + * @throws IOException when an error occurs during streaming operation + * + * @since 2.3M2 + */ + public void setContent(InputStream is, int len) throws IOException + { + setContent(readData(is, len)); + } + + /** + * @return the true size of the content of the attachement + * + * @since 2.3M2 + */ + public int getSize() + { + return content.length; + } + + /** + * Read an input stream into a byte array + * + * @param is the input stream to read + * @param len the len to read + * + * @return a byte array of size len containing the read data + * + * @throws IOException when an error occurs during streaming operation + * + * @since 2.3M2 + */ + private byte[] readData(InputStream is, int len) throws IOException + { + if (is == null) + return null; + + int off = 0; + byte[] buf = new byte[len]; + while (len > 0) { + int n = is.read(buf, off, len); + if (n == -1) { + throw new EOFException(); + } + off += n; + len -= n; + } + return buf; + } } Index: src/main/java/com/xpn/xwiki/doc/XWikiAttachment.java =================================================================== --- src/main/java/com/xpn/xwiki/doc/XWikiAttachment.java (revision 27944) +++ src/main/java/com/xpn/xwiki/doc/XWikiAttachment.java (working copy) @@ -21,9 +21,10 @@ package com.xpn.xwiki.doc; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.StringReader; -import java.io.StringWriter; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -39,12 +40,13 @@ import org.dom4j.dom.DOMElement; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; -import org.dom4j.io.XMLWriter; import org.suigeneris.jrcs.rcs.Archive; import org.suigeneris.jrcs.rcs.Version; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.util.DOMXMLWriter; +import com.xpn.xwiki.util.XMLWriter; public class XWikiAttachment implements Cloneable { @@ -142,11 +144,19 @@ return attachment; } + /** + * @return the cached filesize in byte of the attachement, stored as metadata + */ public int getFilesize() { return this.filesize; } + /** + * Set cached filesize of the attachement that will be stored as metadata + * + * @param filesize in byte + */ public void setFilesize(int filesize) { if (filesize != this.filesize) { @@ -156,6 +166,24 @@ this.filesize = filesize; } + /** + * @param context current XWikiContext + * + * @return the real filesize in byte of the attachement. We cannot trust the metadata that may be publicly changed. + * + * @throws XWikiException + * + * @since 2.3M2 + */ + public int getContentSize(XWikiContext context) throws XWikiException + { + if (this.attachment_content == null) { + this.doc.loadAttachmentContent(this, context); + } + + return this.attachment_content.getSize(); + } + public String getFilename() { return this.filename; @@ -283,63 +311,91 @@ this.isMetaDataDirty = metaDataDirty; } + /** + * Retrieve an attachement as an XML string. You should prefer + * {@link #toXML(com.xpn.xwiki.util.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext) to avoid memory loads + * when appropriate. + * + * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded) + * @param bWithVersions if true, all archived versions are also included + * @param context current XWikiContext + * + * @return a string containing an XML representation of the attachment + * + * @throws XWikiException when an error occurs during wiki operations + */ public String toStringXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context) throws XWikiException { - // implement - Element ele = toXML(bWithAttachmentContent, bWithVersions, context); - - Document doc = new DOMDocument(); - doc.setRootElement(ele); - OutputFormat outputFormat = new OutputFormat("", true); - if ((context == null) || (context.getWiki() == null)) - outputFormat.setEncoding("UTF-8"); - else - outputFormat.setEncoding(context.getWiki().getEncoding()); - StringWriter out = new StringWriter(); - XMLWriter writer = new XMLWriter(out, outputFormat); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { - writer.write(doc); - return out.toString(); + XMLWriter wr = new XMLWriter(baos, new OutputFormat("", true, context.getWiki().getEncoding())); + Document doc = new DOMDocument(); + wr.writeDocumentStart(doc); + toXML(wr, bWithAttachmentContent, bWithVersions, context); + wr.writeDocumentEnd(doc); + return baos.toString(context.getWiki().getEncoding()); } catch (IOException e) { e.printStackTrace(); return ""; } } + /** + * Retrieve XML representation of attachement's metadata into an {@link Element}. + * + * @return a {@link Element} containing an XML representation of the attachment without content + * + * @throws XWikiException when an error occurs during wiki operations + */ public Element toXML(XWikiContext context) throws XWikiException { return toXML(false, false, context); } - public Element toXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context) - throws XWikiException + /** + * Write an XML representation of the attachement into an {@link com.xpn.xwiki.util.XMLWriter} + * + * @param wr the XMLWriter to write to + * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded) + * @param bWithVersions if true, all archive version is also included + * @param context current XWikiContext + * + * @throws IOException when an error occurs during streaming operation + * @throws XWikiException when an error occurs during xwiki operation + * + * @since 2.3M2 + */ + public void toXML(XMLWriter wr, boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context) + throws IOException, XWikiException { Element docel = new DOMElement("attachment"); + wr.writeOpen(docel); + Element el = new DOMElement("filename"); el.addText(getFilename()); - docel.add(el); + wr.write(el); el = new DOMElement("filesize"); el.addText("" + getFilesize()); - docel.add(el); + wr.write(el); el = new DOMElement("author"); el.addText(getAuthor()); - docel.add(el); + wr.write(el); long d = getDate().getTime(); el = new DOMElement("date"); el.addText("" + d); - docel.add(el); + wr.write(el); el = new DOMElement("version"); el.addText(getVersion()); - docel.add(el); + wr.write(el); el = new DOMElement("comment"); el.addText(getComment()); - docel.add(el); + wr.write(el); if (bWithAttachmentContent) { el = new DOMElement("content"); @@ -347,13 +403,11 @@ loadContent(context); XWikiAttachmentContent acontent = getAttachment_content(); if (acontent != null) { - byte[] bcontent = getAttachment_content().getContent(); - String content = new String(Base64.encodeBase64(bcontent)); - el.addText(content); + wr.writeBase64(el, getAttachment_content().getContentInputStream()); } else { el.addText(""); + wr.write(el); } - docel.add(el); } if (bWithVersions) { @@ -363,16 +417,44 @@ el = new DOMElement("versions"); try { el.addText(new String(aarchive.getArchive())); + wr.write(el); } catch (XWikiException e) { - return null; } - docel.add(el); } } - return docel; + wr.writeClose(docel); } + /** + * Retrieve XML representation of attachements metadata into an {@link Element}. You should prefer + * {@link #toXML(com.xpn.xwiki.util.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext) to avoid memory loads + * when appropriate. + * + * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded) + * @param bWithVersions if true, all archived versions are also included + * @param context current XWikiContext + * + * @return an {@link Element} containing an XML representation of the attachment + * + * @throws XWikiException when an error occurs during wiki operations + * + * @since 2.3M2 + */ + public Element toXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context) + throws XWikiException + { + Document doc = new DOMDocument(); + DOMXMLWriter wr = new DOMXMLWriter(doc, new OutputFormat("", true, context.getWiki().getEncoding())); + + try { + toXML(wr, bWithAttachmentContent, bWithVersions, context); + } catch (IOException e) { + throw new RuntimeException(e); + } + return doc.getRootElement(); + } + public void fromXML(String data) throws XWikiException { SAXReader reader = new SAXReader(); @@ -433,6 +515,18 @@ this.attachment_archive = attachment_archive; } + /** + * Retrive the content of this attachment as a byte array. + * + * @param context current XWikiContext + * + * @return a byte array containing the binary data content of the attachment + * + * @throws XWikiException when an error occurs during wiki operation + * + * @deprecated use {@link #setContent(java.io.InputStream, int)} instead + */ + @Deprecated public byte[] getContent(XWikiContext context) throws XWikiException { if (this.attachment_content == null) { @@ -442,6 +536,26 @@ return this.attachment_content.getContent(); } + /** + * Retrive the content of this attachment as an input stream. + * + * @param context current XWikiContext + * + * @return an InputStream to consume for receiving the content of this attachment + * + * @throws XWikiException when an error occurs during wiki operation + * + * @since 2.3M2 + */ + public InputStream getContentInputStream(XWikiContext context) throws XWikiException + { + if (this.attachment_content == null) { + this.doc.loadAttachmentContent(this, context); + } + + return this.attachment_content.getContentInputStream(); + } + public Archive getArchive() { if (this.attachment_archive == null) { @@ -500,6 +614,14 @@ return list; } + /** + * Set the content of an attachement from a byte array. + * + * @param data a byte array with the binary content of the attachment + * + * @deprecated use {@link #setContent(java.io.InputStream, int)} instead + */ + @Deprecated public void setContent(byte[] data) { if (this.attachment_content == null) { @@ -510,6 +632,26 @@ this.attachment_content.setContent(data); } + /** + * Set the content of an attachment from an InputStream. + * + * @param is the input stream that will be read + * @param length the length in byte to read + * + * @throws IOException when an error occurs during streaming operation + * + * @since 2.3M2 + */ + public void setContent(InputStream is, int length) throws IOException + { + if (this.attachment_content == null) { + this.attachment_content = new XWikiAttachmentContent(); + this.attachment_content.setAttachment(this); + } + + this.attachment_content.setContent(is, length); + } + public void loadContent(XWikiContext context) throws XWikiException { if (this.attachment_content == null) { Index: src/main/java/com/xpn/xwiki/doc/XWikiDocument.java =================================================================== --- src/main/java/com/xpn/xwiki/doc/XWikiDocument.java (revision 27944) +++ src/main/java/com/xpn/xwiki/doc/XWikiDocument.java (working copy) @@ -22,8 +22,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringReader; -import java.io.StringWriter; import java.lang.ref.SoftReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -66,7 +66,6 @@ import org.dom4j.dom.DOMElement; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; -import org.dom4j.io.XMLWriter; import org.suigeneris.jrcs.diff.Diff; import org.suigeneris.jrcs.diff.DifferentiationFailedException; import org.suigeneris.jrcs.diff.Revision; @@ -135,7 +134,9 @@ import com.xpn.xwiki.store.XWikiStoreInterface; import com.xpn.xwiki.store.XWikiVersioningStoreInterface; import com.xpn.xwiki.user.api.XWikiRightService; +import com.xpn.xwiki.util.DOMXMLWriter; import com.xpn.xwiki.util.Util; +import com.xpn.xwiki.util.XMLWriter; import com.xpn.xwiki.validation.XWikiValidationInterface; import com.xpn.xwiki.validation.XWikiValidationStatus; import com.xpn.xwiki.web.EditForm; @@ -3351,108 +3352,270 @@ return true; } + /** + * Convert a {@link Document} into an XML string. You should prefer + * {@link #toXML(OutputStream, boolean, boolean, boolean, boolean, XWikiContext)} or + * {@link #toXML(com.xpn.xwiki.util.XMLWriter, boolean, boolean, boolean, boolean, XWikiContext)} when possible + * to avoid memory load. + * + * @param doc the {@link Document} to convert to a String + * @param context current XWikiContext + * + * @return an XML representation of the {@link Document} + * + * @deprecated this method has nothing to do here and is apparently unused + */ + @Deprecated public String toXML(Document doc, XWikiContext context) { - OutputFormat outputFormat = new OutputFormat("", true); - if ((context == null) || (context.getWiki() == null)) { - outputFormat.setEncoding("UTF-8"); - } else { - outputFormat.setEncoding(context.getWiki().getEncoding()); - } + String encoding = context.getWiki().getEncoding(); - StringWriter out = new StringWriter(); - XMLWriter writer = new XMLWriter(out, outputFormat); + ByteArrayOutputStream os = new ByteArrayOutputStream(); try { - writer.write(doc); - return out.toString(); + XMLWriter wr = new XMLWriter(os, new OutputFormat("", true, encoding)); + wr.write(doc); + return os.toString(encoding); } catch (IOException e) { - e.printStackTrace(); + LOG.error("Exception while doc.toXML", e); return ""; } } + /** + * Retrieve the document in the current context language as an XML string. The rendrered document content and + * all XObjects are included. Document attachments and archived versions are excluded. + * You should prefer toXML(OutputStream, true, true, false, false, XWikiContext)} or + * toXML(com.xpn.xwiki.util.XMLWriter, true, true, false, false, XWikiContext) on the translated document + * when possible to reduce memory load. + * + * @param context current XWikiContext + * + * @return a string containing an XML representation of the document in the current context language + * + * @throws XWikiException when an error occurs during wiki operation + */ public String getXMLContent(XWikiContext context) throws XWikiException { XWikiDocument tdoc = getTranslatedDocument(context); - Document doc = tdoc.toXMLDocument(true, true, false, false, context); - - return toXML(doc, context); + return tdoc.toXML(true, true, false, false, context); } + /** + * Retrieve the document as an XML string. All XObject are included. Rendered content, attachments and archived + * version are excluded. + * You should prefer toXML(OutputStream, true, false, false, false, XWikiContext)} or + * toXML(com.xpn.xwiki.util.XMLWriter, true, false, false, false, XWikiContext) when possible to reduce memory load. + * + * @param context current XWikiContext + * + * @return a string containing an XML representation of the document + * + * @throws XWikiException when an error occurs during wiki operation + */ public String toXML(XWikiContext context) throws XWikiException { - Document doc = toXMLDocument(context); - - return toXML(doc, context); + return toXML(true, false, false, false, context); } + /** + * Retrieve the document as an XML string. All XObjects attachments and archived version are included. Rendered + * content is excluded. + * You should prefer toXML(OutputStream, true, false, true, true, XWikiContext)} or + * toXML(com.xpn.xwiki.util.XMLWriter, true, false, true, true, XWikiContext) when possible to reduce memory load. + * + * @param context current XWikiContext + * + * @return a string containing an XML representation of the document + * + * @throws XWikiException when an error occurs during wiki operation + */ public String toFullXML(XWikiContext context) throws XWikiException { return toXML(true, false, true, true, context); } - public void addToZip(ZipOutputStream zos, boolean withVersions, XWikiContext context) throws IOException + /** + * Serialize the document into a new entry of an ZipOutputStream in XML format. All XObjects and attachments are + * included. Rendered content is excluded. + * + * @param zos the ZipOutputStream to write to + * @param zipname the name of the new entry to create + * @param withVersions if true, also include archived version of the document + * @param context current XWikiContext + * + * @throws XWikiException when an error occurs during xwiki operations + * @throws IOException when an error occurs during streaming operations + * + * @since 2.3M2 + */ + public void addToZip(ZipOutputStream zos, String zipname, boolean withVersions, XWikiContext context) + throws XWikiException, IOException { - try { - String zipname = + ZipEntry zipentry = new ZipEntry(zipname); + zos.putNextEntry(zipentry); + toXML(zos, true, false, true, withVersions, context); + zos.closeEntry(); + } + + /** + * Serialize the document into a new entry of an ZipOutputStream in XML format. The new entry is named + * 'LastSpaceName/DocumentName'. All XObjects and attachments are included. Rendered content is excluded. + * + * @param zos the ZipOutputStream to write to + * @param withVersions if true, also include archived version of the document + * @param context current XWikiContext + * + * @throws XWikiException when an error occurs during xwiki operations + * @throws IOException when an error occurs during streaming operations + * + * @since 2.3M2 + */ + public void addToZip(ZipOutputStream zos, boolean withVersions, XWikiContext context) throws XWikiException, + IOException + { + String zipname = getDocumentReference().getLastSpaceReference().getName() + "/" + getDocumentReference().getName(); - String language = getLanguage(); - if (!StringUtils.isEmpty(language)) { - zipname += "." + language; - } - ZipEntry zipentry = new ZipEntry(zipname); - zos.putNextEntry(zipentry); - zos.write(toXML(true, false, true, withVersions, context).getBytes()); - zos.closeEntry(); - } catch (Exception e) { - e.printStackTrace(); + String language = getLanguage(); + if (!StringUtils.isEmpty(language)) { + zipname += "." + language; } + addToZip(zos, zipname, withVersions, context); } - public void addToZip(ZipOutputStream zos, XWikiContext context) throws IOException + /** + * Serialize the document into a new entry of an ZipOutputStream in XML format. The new entry is named + * 'LastSpaceName/DocumentName'. All XObjects, attachments and archived versions are included. Rendered content + * is excluded. + * + * @param zos the ZipOutputStream to write to + * @param withVersions if true, also include archived version of the document + * @param context current XWikiContext + * + * @throws XWikiException when an error occurs during xwiki operations + * @throws IOException when an error occurs during streaming operations + * + * @since 2.3M2 + */ + public void addToZip(ZipOutputStream zos, XWikiContext context) throws XWikiException, IOException { addToZip(zos, true, context); } + /** + * Serialize the document to an XML string. + * You should prefer {@link #toXML(OutputStream, boolean, boolean, boolean, boolean, XWikiContext)} or + * {@link #toXML(com.xpn.xwiki.util.XMLWriter, boolean, boolean, boolean, boolean, XWikiContext)} when possible to + * reduce memory load. + * + * @param bWithObjects include XObjects + * @param bWithRendering include the rendered content + * @param bWithAttachmentContent include attachments content + * @param bWithVersions include archived versions + * @param context current XWikiContext + * + * @return a string containing an XML representation of the document + * + * @throws XWikiException when an errors occurs during wiki operations + */ public String toXML(boolean bWithObjects, boolean bWithRendering, boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context) throws XWikiException { - Document doc = toXMLDocument(bWithObjects, bWithRendering, bWithAttachmentContent, bWithVersions, context); - - return toXML(doc, context); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + toXML(baos, bWithObjects, bWithRendering, bWithAttachmentContent, bWithVersions, context); + return baos.toString(context.getWiki().getEncoding()); + } catch (IOException e) { + e.printStackTrace(); + return ""; + } } + /** + * Serialize the document to an XML {@link DOMDocument}. All XObject are included. Rendered content, attachments + * and archived version are excluded. + * You should prefer toXML(OutputStream, true, false, false, false, XWikiContext)} or + * toXML(com.xpn.xwiki.util.XMLWriter, true, false, false, false, XWikiContext) when possible to reduce memory load. + * + * @param context current XWikiContext + * + * @return a {@link DOMDocument} containing the serialized document. + * + * @throws XWikiException when an errors occurs during wiki operations + */ public Document toXMLDocument(XWikiContext context) throws XWikiException { return toXMLDocument(true, false, false, false, context); } + /** + * Serialize the document to an XML {@link DOMDocument}. + * You should prefer {@link #toXML(OutputStream, boolean, boolean, boolean, boolean, XWikiContext)} or + * {@link #toXML(com.xpn.xwiki.util.XMLWriter, boolean, boolean, boolean, boolean, XWikiContext)} when possible to + * reduce memory load. + * + * @param bWithObjects include XObjects + * @param bWithRendering include the rendered content + * @param bWithAttachmentContent include attachments content + * @param bWithVersions include archived versions + * @param context current XWikiContext + * + * @return a {@link DOMDocument} containing the serialized document. + * + * @throws XWikiException when an errors occurs during wiki operations + */ public Document toXMLDocument(boolean bWithObjects, boolean bWithRendering, boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context) throws XWikiException { Document doc = new DOMDocument(); + DOMXMLWriter wr = new DOMXMLWriter(doc, new OutputFormat("", true, context.getWiki().getEncoding())); + + try { + toXML(wr, bWithObjects, bWithRendering, bWithAttachmentContent, bWithVersions, context); + return doc; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Serialize the document to a {@link com.xpn.xwiki.util.XMLWriter}. + * + * @param bWithObjects include XObjects + * @param bWithRendering include the rendered content + * @param bWithAttachmentContent include attachments content + * @param bWithVersions include archived versions + * @param context current XWikiContext + * + * @throws XWikiException when an errors occurs during wiki operations + * @throws IOException when an errors occurs during streaming operations + * + * @since 2.3M2 + */ + public void toXML(XMLWriter wr, boolean bWithObjects, boolean bWithRendering, boolean bWithAttachmentContent, + boolean bWithVersions, XWikiContext context) throws XWikiException, IOException + { Element docel = new DOMElement("xwikidoc"); - doc.setRootElement(docel); + wr.writeOpen(docel); Element el = new DOMElement("web"); el.addText(getDocumentReference().getLastSpaceReference().getName()); - docel.add(el); + wr.write(el); el = new DOMElement("name"); el.addText(getDocumentReference().getName()); - docel.add(el); + wr.write(el); el = new DOMElement("language"); el.addText(getLanguage()); - docel.add(el); + wr.write(el); el = new DOMElement("defaultLanguage"); el.addText(getDefaultLanguage()); - docel.add(el); + wr.write(el); el = new DOMElement("translation"); el.addText("" + getTranslation()); - docel.add(el); + wr.write(el); el = new DOMElement("parent"); if (getRelativeParentReference() == null) { @@ -3461,46 +3624,46 @@ } else { el.addText(this.defaultEntityReferenceSerializer.serialize(getRelativeParentReference())); } - docel.add(el); + wr.write(el); el = new DOMElement("creator"); el.addText(getCreator()); - docel.add(el); + wr.write(el); el = new DOMElement("author"); el.addText(getAuthor()); - docel.add(el); + wr.write(el); el = new DOMElement("customClass"); el.addText(getCustomClass()); - docel.add(el); + wr.write(el); el = new DOMElement("contentAuthor"); el.addText(getContentAuthor()); - docel.add(el); + wr.write(el); long d = getCreationDate().getTime(); el = new DOMElement("creationDate"); el.addText("" + d); - docel.add(el); + wr.write(el); d = getDate().getTime(); el = new DOMElement("date"); el.addText("" + d); - docel.add(el); + wr.write(el); d = getContentUpdateDate().getTime(); el = new DOMElement("contentUpdateDate"); el.addText("" + d); - docel.add(el); + wr.write(el); el = new DOMElement("version"); el.addText(getVersion()); - docel.add(el); + wr.write(el); el = new DOMElement("title"); el.addText(getTitle()); - docel.add(el); + wr.write(el); el = new DOMElement("template"); if (getTemplateDocumentReference() == null) { @@ -3509,34 +3672,34 @@ } else { el.addText(this.localEntityReferenceSerializer.serialize(getTemplateDocumentReference())); } - docel.add(el); + wr.write(el); el = new DOMElement("defaultTemplate"); el.addText(getDefaultTemplate()); - docel.add(el); + wr.write(el); el = new DOMElement("validationScript"); el.addText(getValidationScript()); - docel.add(el); + wr.write(el); el = new DOMElement("comment"); el.addText(getComment()); - docel.add(el); + wr.write(el); el = new DOMElement("minorEdit"); el.addText(String.valueOf(isMinorEdit())); - docel.add(el); + wr.write(el); el = new DOMElement("syntaxId"); el.addText(getSyntaxId()); - docel.add(el); + wr.write(el); el = new DOMElement("hidden"); el.addText(String.valueOf(isHidden())); - docel.add(el); + wr.write(el); for (XWikiAttachment attach : getAttachmentList()) { - docel.add(attach.toXML(bWithAttachmentContent, bWithVersions, context)); + attach.toXML(wr, bWithAttachmentContent, bWithVersions, context); } if (bWithObjects) { @@ -3544,7 +3707,7 @@ BaseClass bclass = getXClass(); if (bclass.getFieldList().size() > 0) { // If the class has fields, add class definition and field information to XML - docel.add(bclass.toXML(null)); + wr.write(bclass.toXML(null)); } // Add Objects (THEIR ORDER IS MOLDED IN STONE!) @@ -3557,7 +3720,7 @@ } else { objclass = obj.getXClass(context); } - docel.add(obj.toXML(objclass)); + wr.write(obj.toXML(objclass)); } } } @@ -3571,7 +3734,7 @@ // String newcontent = encodedXMLStringAsUTF8(getContent()); String newcontent = this.content; el.addText(newcontent); - docel.add(el); + wr.write(el); if (bWithRendering) { el = new DOMElement("renderedcontent"); @@ -3580,21 +3743,44 @@ } catch (XWikiException e) { el.addText("Exception with rendering content: " + e.getFullMessage()); } - docel.add(el); + wr.write(el); } if (bWithVersions) { el = new DOMElement("versions"); try { el.addText(getDocumentArchive(context).getArchive(context)); - docel.add(el); + wr.write(el); } catch (XWikiException e) { LOG.error("Document [" + this.defaultEntityReferenceSerializer.serialize(getDocumentReference()) + "] has malformed history"); } } + } - return doc; + /** + * Serialize the document to an OutputStream. + * + * @param bWithObjects include XObjects + * @param bWithRendering include the rendered content + * @param bWithAttachmentContent include attachments content + * @param bWithVersions include archived versions + * @param context current XWikiContext + * + * @throws XWikiException when an errors occurs during wiki operations + * @throws IOException when an errors occurs during streaming operations + * + * @since 2.3M2 + */ + public void toXML(OutputStream out, boolean bWithObjects, boolean bWithRendering, boolean bWithAttachmentContent, + boolean bWithVersions, XWikiContext context) throws XWikiException, IOException + { + XMLWriter wr = new XMLWriter(out, new OutputFormat("", true, context.getWiki().getEncoding())); + + Document doc = new DOMDocument(); + wr.writeDocumentStart(doc); + toXML(wr, bWithObjects, bWithRendering, bWithAttachmentContent, bWithVersions, context); + wr.writeDocumentEnd(doc); } protected String encodedXMLStringAsUTF8(String xmlString) Index: src/main/java/com/xpn/xwiki/web/DownloadAction.java =================================================================== --- src/main/java/com/xpn/xwiki/web/DownloadAction.java (revision 27944) +++ src/main/java/com/xpn/xwiki/web/DownloadAction.java (working copy) @@ -28,6 +28,8 @@ import java.io.IOException; +import org.apache.commons.io.IOUtils; + public class DownloadAction extends XWikiAction { /** @@ -94,19 +96,14 @@ response.setDateHeader("Last-Modified", attachment.getDate().getTime()); // Sending the content of the attachment - byte[] data; try { - data = attachment.getContent(context); + response.setContentLength(attachment.getContentSize(context)); + IOUtils.copy(attachment.getContentInputStream(context), response.getOutputStream()); } catch (XWikiException e) { Object[] args = {filename}; throw new XWikiException(XWikiException.MODULE_XWIKI_APP, XWikiException.ERROR_XWIKI_APP_ATTACHMENT_NOT_FOUND, "Attachment content {0} not found", null, args); - } - - response.setContentLength(data.length); - try { - response.getOutputStream().write(data); } catch (IOException e) { throw new XWikiException(XWikiException.MODULE_XWIKI_APP, XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION, Index: src/main/java/com/xpn/xwiki/web/ImportAction.java =================================================================== --- src/main/java/com/xpn/xwiki/web/ImportAction.java (revision 27944) +++ src/main/java/com/xpn/xwiki/web/ImportAction.java (working copy) @@ -72,7 +72,7 @@ response.setContentType("text/xml"); response.setCharacterEncoding(encoding); XWikiAttachment packFile = doc.getAttachment(name); - importer.Import(packFile.getContent(context)); + importer.Import(packFile.getContentInputStream(context)); String xml = importer.toXml(); byte[] result = xml.getBytes(encoding); response.setContentLength(result.length); @@ -81,7 +81,7 @@ } else if ("import".equals(action)) { // Do the actual import XWikiAttachment packFile = doc.getAttachment(name); - importer.Import(packFile.getContent(context)); + importer.Import(packFile.getContentInputStream(context)); String all = request.get("all"); if (!"1".equals(all)) { if (pages != null) { Index: src/main/java/com/xpn/xwiki/web/UploadAction.java =================================================================== --- src/main/java/com/xpn/xwiki/web/UploadAction.java (revision 27944) +++ src/main/java/com/xpn/xwiki/web/UploadAction.java (working copy) @@ -21,6 +21,7 @@ package com.xpn.xwiki.web; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -150,8 +151,6 @@ XWikiResponse response = context.getResponse(); String username = context.getUser(); - byte[] data = fileupload.getFileItemData(fieldName, context); - // Read XWikiAttachment XWikiAttachment attachment = doc.getAttachment(filename); @@ -159,7 +158,21 @@ attachment = new XWikiAttachment(); doc.getAttachmentList().add(attachment); } - attachment.setContent(data); + + int fisize = fileupload.getFileItemSize(fieldName, context); + + try { + if (fisize > 0) { + InputStream fiis = fileupload.getFileItemInputStream(fieldName, context); + attachment.setContent(fiis, fisize); + } else { + attachment.setContent(new byte[0]); + } + } catch (IOException e) { + throw new XWikiException(XWikiException.MODULE_XWIKI_APP, + XWikiException.ERROR_XWIKI_APP_UPLOAD_FILE_EXCEPTION, "Exception while reading uploaded parsed file", e); + } + attachment.setFilename(filename); attachment.setAuthor(username); Index: src/main/java/com/xpn/xwiki/web/DownloadRevAction.java =================================================================== --- src/main/java/com/xpn/xwiki/web/DownloadRevAction.java (revision 27944) +++ src/main/java/com/xpn/xwiki/web/DownloadRevAction.java (working copy) @@ -9,6 +9,8 @@ import com.xpn.xwiki.plugin.XWikiPluginManager; import com.xpn.xwiki.util.Util; +import org.apache.commons.io.IOUtils; + public class DownloadRevAction extends XWikiAction { @Override @@ -70,10 +72,9 @@ response.setDateHeader("Last-Modified", attachment.getDate().getTime()); // Sending the content of the attachment - byte[] data = attachment.getContent(context); - response.setContentLength(data.length); try { - response.getOutputStream().write(data); + response.setContentLength(attachment.getContentSize(context)); + IOUtils.copy(attachment.getContentInputStream(context), response.getOutputStream()); } catch (IOException e) { throw new XWikiException(XWikiException.MODULE_XWIKI_APP, XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION, "Exception while sending response", e); Index: src/main/java/com/xpn/xwiki/web/ExportURLFactory.java =================================================================== --- src/main/java/com/xpn/xwiki/web/ExportURLFactory.java (revision 27944) +++ src/main/java/com/xpn/xwiki/web/ExportURLFactory.java (working copy) @@ -17,6 +17,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.commons.io.IOUtils; + import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiAttachment; @@ -342,9 +344,8 @@ context.getWiki().getDocument( db + XWikiDocument.DB_SPACE_SEP + space + XWikiDocument.SPACE_NAME_SEP + name, context); XWikiAttachment attachment = doc.getAttachment(filename); - byte[] data = attachment.getContent(context); FileOutputStream fos = new FileOutputStream(file); - fos.write(data); + IOUtils.copy(attachment.getContentInputStream(context), fos); fos.close(); } Index: src/main/java/com/xpn/xwiki/web/SkinAction.java =================================================================== --- src/main/java/com/xpn/xwiki/web/SkinAction.java (revision 27944) +++ src/main/java/com/xpn/xwiki/web/SkinAction.java (working copy) @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Date; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -323,15 +324,18 @@ if (attachment != null) { XWiki xwiki = context.getWiki(); XWikiResponse response = context.getResponse(); - byte[] data = attachment.getContent(context); String mimetype = xwiki.getEngineContext().getMimeType(filename.toLowerCase()); if (isCssMimeType(mimetype) || isJavascriptMimeType(mimetype)) { + byte[] data = attachment.getContent(context); // Always force UTF-8, as this is the assumed encoding for text files. data = context.getWiki().parseContent(new String(data, ENCODING), context).getBytes(ENCODING); response.setCharacterEncoding(ENCODING); + setupHeaders(response, mimetype, attachment.getDate(), data.length); + response.getOutputStream().write(data); + } else { + setupHeaders(response, mimetype, attachment.getDate(), attachment.getContentSize(context)); + IOUtils.copy(attachment.getContentInputStream(context), response.getOutputStream()); } - setupHeaders(response, mimetype, attachment.getDate(), data.length); - response.getOutputStream().write(data); return true; } else { LOG.debug("Attachment not found"); Index: src/main/java/com/xpn/xwiki/store/jcr/XWikiJcrAttachmentStore.java =================================================================== --- src/main/java/com/xpn/xwiki/store/jcr/XWikiJcrAttachmentStore.java (revision 27944) +++ src/main/java/com/xpn/xwiki/store/jcr/XWikiJcrAttachmentStore.java (working copy) @@ -43,7 +43,7 @@ Node dirattNode = JcrUtil.getOrCreateSubNode(docNode, "attach", ntXWikiAttachments); Node attachNode = JcrUtil.getOrCreateSubNode(dirattNode, attachment.getFilename(), ntXWikiAttachment); Node attachContentNode = JcrUtil.getOrCreateSubNode(attachNode, "content", ntXWikiAttachmentContent); - attachContentNode.setProperty("jcr:data", new ByteArrayInputStream(content.getContent())); + attachContentNode.setProperty("jcr:data", content.getContentInputStream()); attachContentNode.setProperty("attach", attachNode); Node attachArchiveNode = JcrUtil.getOrCreateSubNode(attachNode, "archive", ntXWikiAttachmentArchive); attachArchiveNode.setProperty("jcr:data", new ByteArrayInputStream(archive.getArchive())); Index: src/main/java/com/xpn/xwiki/pdf/impl/PdfURLFactory.java =================================================================== --- src/main/java/com/xpn/xwiki/pdf/impl/PdfURLFactory.java (revision 27944) +++ src/main/java/com/xpn/xwiki/pdf/impl/PdfURLFactory.java (working copy) @@ -30,6 +30,8 @@ import java.io.FileOutputStream; import java.net.URL; +import org.apache.commons.io.IOUtils; + public class PdfURLFactory extends XWikiServletURLFactory { public PdfURLFactory() @@ -46,10 +48,8 @@ XWikiDocument doc = null; doc = context.getWiki().getDocument(web + "." + name, context); XWikiAttachment attachment = doc.getAttachment(filename); - byte[] data = new byte[0]; - data = attachment.getContent(context); FileOutputStream fos = new FileOutputStream(file); - fos.write(data); + IOUtils.copy(attachment.getContentInputStream(context), fos); fos.close(); } return file.toURL(); @@ -69,10 +69,8 @@ XWikiDocument doc = null; doc = context.getWiki().getDocument(web + "." + name, context); XWikiAttachment attachment = doc.getAttachment(filename).getAttachmentRevision(revision, context); - byte[] data = new byte[0]; - data = attachment.getContent(context); FileOutputStream fos = new FileOutputStream(file); - fos.write(data); + IOUtils.copy(attachment.getContentInputStream(context), fos); fos.close(); } return file.toURL(); Index: src/main/java/com/xpn/xwiki/util/UnclosableInputStream.java =================================================================== --- src/main/java/com/xpn/xwiki/util/UnclosableInputStream.java (revision 0) +++ src/main/java/com/xpn/xwiki/util/UnclosableInputStream.java (revision 0) @@ -0,0 +1,54 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This 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 software 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 software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ +package com.xpn.xwiki.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Utility filter stream that prevent closing of the embedded input stream + * + * @author Denis Gervalle + * @version $Id: $ + */ +public class UnclosableInputStream extends FilterInputStream +{ + + /** + * @param in the stream to embed and avoid closing on + */ + public UnclosableInputStream(InputStream in) + { + super(in); + } + + /** + * Do not close anything + * + * @see java.io.FilterInputStream#close() + */ + @Override + public void close() throws IOException + { + // Dot close embedded stream + } +} Index: src/main/java/com/xpn/xwiki/util/UnclosableOutputStream.java =================================================================== --- src/main/java/com/xpn/xwiki/util/UnclosableOutputStream.java (revision 0) +++ src/main/java/com/xpn/xwiki/util/UnclosableOutputStream.java (revision 0) @@ -0,0 +1,54 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This 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 software 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 software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ +package com.xpn.xwiki.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Utility filter stream that prevent closing of the embedded output stream + * + * @author Denis Gervalle + * @version $Id: $ + */ +public class UnclosableOutputStream extends FilterOutputStream +{ + + /** + * @param out the stream to embed and avoid closing on + */ + public UnclosableOutputStream(OutputStream out) + { + super(out); + } + + /** + * Do not close anything + * + * @see java.io.FilterOutputStream#close() + */ + @Override + public void close() throws IOException + { + // Dot close embedded stream + } +} Index: src/main/java/com/xpn/xwiki/util/DOMXMLWriter.java =================================================================== --- src/main/java/com/xpn/xwiki/util/DOMXMLWriter.java (revision 0) +++ src/main/java/com/xpn/xwiki/util/DOMXMLWriter.java (revision 0) @@ -0,0 +1,246 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This 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 software 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 software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xpn.xwiki.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.util.EmptyStackException; + +import org.apache.commons.codec.binary.Base64OutputStream; +import org.apache.commons.io.IOUtils; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.dom.DOMDocument; +import org.dom4j.io.OutputFormat; + +/** + * This a minimal implementation to transform an XMLWriter into a {@link DOMDocument} builder. + *

+ * This implementation allow the use of a same function accepting an XMLWriter, to either produce output into an + * {@link OutputStream} or to create a {@link DOMDocument}. Here a sample of the way to do so: + *

+ *
+ *     public void toXML(XMLWriter wr) throws IOException
+ *     {
+ *          Element docel = new DOMElement("html");
+ *          wr.writeOpen(docel);
+ *          Element hbel = new DOMElement("head");
+ *          wr.writeOpen(hbel);
+ *          Element el = new DOMElement("title");
+ *          el.addText("My Title");
+ *          wr.write(el);
+ *          wr.writeClose(hbel);
+ *          hbel = new DOMElement("body");
+ *          wr.writeOpen(hbel);
+ *          el = new DOMElement("p");
+ *          el.addText("My Body");
+ *          wr.write(el);
+ *     }
+ * 
+ *     public void toXML(OutputStream out) throws IOException
+ *     {
+ *          XMLWriter wr = new XMLWriter(out, new OutputFormat("  ", true, "UTF-8"));
+ * 
+ *          Document doc = new DOMDocument();
+ *          wr.writeDocumentStart(doc);
+ *          toXML(wr);
+ *          wr.writeDocumentEnd(doc);
+ *     }
+ * 
+ *     public Document toXMLDocument()
+ *     {
+ *          Document doc = new DOMDocument();
+ *          DOMXMLWriter wr = new DOMXMLWriter(doc, new OutputFormat("  ", true, "UTF-8"));
+ * 
+ *          try {
+ *              toXML(wr);
+ *              return doc;
+ *          } catch (IOException e) {
+ *              throw new RuntimeException(e);
+ *          }
+ *     }
+ * 
+ *

+ * WARNING - This implementation in INCOMPLETE and a minimal support. It should be improve as needed over time + *

+ * + * @author Denis Gervalle + * @version $Id: $ + */ +public class DOMXMLWriter extends XMLWriter +{ + /** + * The {@link Document} currently built by this writer + */ + private Document doc; + + /** + * The {@link OutputFormat} providing the encoding requested + */ + private OutputFormat format; + + /** + * Create a new {@link DOMXMLWriter} that will build into the provided document using the default + * encoding. + * + * @param doc {@link Document} that will be build by this writer + */ + public DOMXMLWriter(Document doc) + { + this.format = DEFAULT_FORMAT; + this.doc = doc; + } + + /** + * Create a new {@link DOMXMLWriter} that will build into the provided document using the encoding + * provided in the {@link OutputFormat}. + * + * @param doc {@link Document} that will be build by this writer + * @param format {@link OutputFormat} used to retrieve the proper encoding + */ + public DOMXMLWriter(Document doc, OutputFormat format) + { + this.format = format; + this.doc = doc; + } + + /** + * Add the element into the {@link Document} as a children of the element at the top of the stack of + * opened elements, putting the whole stream content as text in the content of the {@link Element}. The + * stream is converted to a String encoded in the current encoding. + * + * @see com.xpn.xwiki.util.XMLWriter#write(org.dom4j.Element, java.io.InputStream) + */ + @Override + public void write(Element element, InputStream is) throws IOException + { + element.addText(IOUtils.toString(is, format.getEncoding())); + write(element); + } + + /** + * Add the element into the {@link Document} as a children of the element at the top of the stack of of + * the {@link Element}. + * + * @see com.xpn.xwiki.util.XMLWriter#write(org.dom4j.Element, java.io.Reader) + */ + @Override + public void write(Element element, Reader rd) throws IOException + { + element.addText(IOUtils.toString(rd)); + write(element); + } + + /** + * Add the element into the {@link Document} as a children of the element at the top of the stack of + * opened elements, putting the whole stream content as Base64 text in the content of the + * {@link Element}. + * + * @see com.xpn.xwiki.util.XMLWriter#writeBase64(org.dom4j.Element, java.io.InputStream) + */ + @Override + public void writeBase64(Element element, InputStream is) throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64OutputStream out = new Base64OutputStream(baos, true, 0, null); + IOUtils.copy(is, out); + out.close(); + element.addText(baos.toString(format.getEncoding())); + write(element); + } + + /** + * Cleanup the stack of opened elements. + * + * @see com.xpn.xwiki.util.XMLWriter#writeDocumentEnd(org.dom4j.Document) + */ + @Override + public void writeDocumentEnd(Document doc) throws IOException + { + if (!parent.isEmpty()) { + writeClose(parent.firstElement()); + } + } + + /** + * Does nothing, avoid default action + * + * @see com.xpn.xwiki.util.XMLWriter#writeDocumentStart(org.dom4j.Document) + */ + @Override + public void writeDocumentStart(Document doc) throws IOException + { + } + + /** + * Add the element into the {@link Document} as a children of the element at the top of the stack of + * opened elements. + * + * @see org.dom4j.io.XMLWriter#write(org.dom4j.Element) + */ + @Override + public void write(Element element) throws IOException + { + if (parent.isEmpty()) { + doc.setRootElement(element); + } else { + parent.add(element); + } + } + + /** + * Cleanup the stack of opened elements up to the given element. + * + * @see org.dom4j.io.XMLWriter#writeClose(org.dom4j.Element) + */ + @Override + public void writeClose(Element element) throws IOException + { + try { + while (parent.peek() != element) { + parent.pop(); + } + parent.pop(); + } catch (EmptyStackException e) { + throw new IOException("FATAL: Closing a element that have never been opened", e); + } + } + + /** + * Add the element into the {@link Document} as a children of the element at the top of the stack of + * opened elements. Add this element at the top of the stack of opened elements. + * + * @see org.dom4j.io.XMLWriter#writeOpen(org.dom4j.Element) + */ + @Override + public void writeOpen(Element element) throws IOException + { + if (parent.isEmpty()) { + doc.setRootElement(element); + } else { + parent.peek().add(element); + } + + parent.push(element); + } +} Index: src/main/java/com/xpn/xwiki/util/XMLWriter.java =================================================================== --- src/main/java/com/xpn/xwiki/util/XMLWriter.java (revision 0) +++ src/main/java/com/xpn/xwiki/util/XMLWriter.java (revision 0) @@ -0,0 +1,227 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This 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 software 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 software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + * + */ +package com.xpn.xwiki.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.Stack; + +import org.apache.commons.codec.binary.Base64OutputStream; +import org.apache.commons.io.IOUtils; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.OutputFormat; + +/** + * Extension to {@link org.dom4j.io.XMLWriter} to allow filling some element content with an input stream, + * minimizing the memory footprint of the operation. + *

+ * This extension is not intended to be used to format a DOM4J tree to a stream, but to immediately write out the tags + * produced without building the document tree in memory. It is not compatible with the SAX part of the original + * {@link org.dom4j.io.XMLWriter} which is itself incompatible with its own DOM part. + *

+ *

+ * An improvement to the writeOpen/writeClose functions ensure better handling of independent opening and closing of + * tags by maintaining a state stack of opened tags. New writeDocumentStart/End function also ensure proper starting and + * ending of the document it self. + *

+ * + * @author Denis Gervalle + * @version $Id: $ + */ +public class XMLWriter extends org.dom4j.io.XMLWriter +{ + /** + * {@link Stack} of currently opened {@link Element}, the first + * {@link Element} is the document root element, and the top of the stack is the last opened + * {@link Element}. + */ + protected Stack parent = new Stack(); + + /** + * Current {@link OutputStream} of this writer + */ + private OutputStream out; + + /** + * Default constructor used by {@link DOMXMLWriter}. + * + * @see DOMXMLWriter + */ + protected XMLWriter() + { + } + + /** + * Create a new XMLWriter writing to a provided OutputStream in a given format. Note that other constructor of the + * original DOM4J XMLWriter are unsupported since a OutputStream is the only way we can support the extensions + * provided here. + *

+ * Note that the writer is buffered and only a call to flush() or writeDocuemntEnd() will ensure the output has been + * fully written to the {@link OutputStream}. + *

+ * + * @param out an {@link OutputStream} where to output the XML produced. + * @param format an {@link OutputFormat} defining the encoding that should be used and esthetics of the + * produced XML. + * @throws UnsupportedEncodingException the requested encoding is unsupported. + */ + public XMLWriter(OutputStream out, OutputFormat format) throws UnsupportedEncodingException + { + super(out, format); + this.out = out; + } + + /** + * Write the {@link Document} declaration, and its {@link DocumentType} if available to + * the output stream. + * + * @param doc {@link Document} to be started, may specify a {@link DocumentType}. + * @throws IOException a problem occurs during writing + */ + public void writeDocumentStart(Document doc) throws IOException + { + writeDeclaration(); + + if (doc.getDocType() != null) { + indent(); + writeDocType(doc.getDocType()); + } + } + + /** + * Close all remaining opened {@link Element} including the root element to terminate the current + * document. Also flush the writer to ensure the whole document has been written to the + * {@link OutputStream}. + * + * @param doc {@link Document} to be end, actually unused. + * @throws IOException a problem occurs during writing. + */ + public void writeDocumentEnd(Document doc) throws IOException + { + if (!parent.isEmpty()) { + writeClose(parent.firstElement()); + } + writePrintln(); + flush(); + } + + /** + * Writes the {@link Element}, including its {@link + * Attribute}s, using the {@link Reader} + * for its content. + *

+ * Note that proper decoding/encoding will occurs during this operation, converting the encoding of the Reader into + * the encoding of the Writer. + *

+ * + * @param element {@link Element} to output. + * @param rd {@link Reader} that will be fully read and transfered into the element content. + * @throws IOException a problem occurs during reading or writing. + */ + public void write(Element element, Reader rd) throws IOException + { + writeOpen(element); + IOUtils.copy(rd, writer); + writeClose(element); + } + + /** + * Writes the {@link Element}, including its {@link + * Attribute}s, using the + * {@link InputStream} for its content. + *

+ * Note that no decoding/encoding of the InputStream will be ensured during this operation. The byte content is + * transfered untouched. + *

+ * + * @param element {@link Element} to output. + * @param is {@link InputStream} that will be fully read and transfered into the element content. + * @throws IOException a problem occurs during reading or writing. + */ + public void write(Element element, InputStream is) throws IOException + { + writeOpen(element); + flush(); + IOUtils.copy(is, out); + writeClose(element); + } + + /** + * Writes the {@link Element}, including its {@link + * Attribute}s, using the + * {@link InputStream} encoded in Base64 for its content. + * + * @param element {@link Element} to output. + * @param is {@link InputStream} that will be fully read and encoded in Base64 into the element + * content. + * @throws IOException a problem occurs during reading or writing. + */ + public void writeBase64(Element element, InputStream is) throws IOException + { + writeOpen(element); + flush(); + Base64OutputStream base64 = new Base64OutputStream(new UnclosableOutputStream(out), true, 0, null); + IOUtils.copy(is, base64); + base64.close(); + writeClose(element); + } + + /** + * Writes the opening tag of an {@link Element}, including its {@link Attribute}s but without its content. + *

+ * Compared to the DOM4J implementation, this function keeps track of opened elements. + *

+ * + * @param elemen {@link Element} to output. + * @throws IOException a problem occurs during writing. + * @see org.dom4j.io.XMLWriter#writeOpen(org.dom4j.Element) + */ + @Override + public void writeOpen(Element element) throws IOException + { + super.writeOpen(element); + parent.push(element); + } + + /** + * Writes the closing tag of an {@link Element}. + *

+ * Compared to the DOM4J implementation, this function ensure closing of all opened element including the one that + * is requested to be closed. + *

+ * + * @param elemen {@link Element} to output. + * @throws IOException a problem occurs during writing. + * @see org.dom4j.io.XMLWriter#writeClose(org.dom4j.Element) + */ + @Override + public void writeClose(Element element) throws IOException + { + while (parent.peek() != element) { + super.writeClose(parent.pop()); + } + super.writeClose(parent.pop()); + } +}