From e047e2509451aa88a04ef67ea5f6399ad9f565cb Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Wed, 9 Apr 2008 12:22:23 +0000 Subject: [PATCH] Tweak how we do ooxml properties, and handle hyperlinks for word documents when extracting git-svn-id: https://svn.apache.org/repos/asf/poi/branches/ooxml@646298 13f79535-47bb-0310-9956-ffa450edef68 --- .../java/org/apache/poi/POIXMLDocument.java | 36 ++--- .../java/org/apache/poi/POIXMLProperties.java | 124 ++++++++++++++++++ .../org/apache/poi/POIXMLTextExtractor.java | 19 +++ .../org/apache/poi/xwpf/XWPFDocument.java | 16 +++ .../poi/xwpf/extractor/XWPFWordExtractor.java | 38 +++++- .../apache/poi/xslf/TestXSLFSlideShow.java | 16 +-- .../org/apache/poi/xwpf/TestXWPFDocument.java | 28 ++-- .../xwpf/extractor/TestXWPFWordExtractor.java | 40 ++++++ .../apache/poi/hwpf/data/TestDocument.docx | Bin 0 -> 10369 bytes 9 files changed, 268 insertions(+), 49 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/POIXMLProperties.java create mode 100755 src/scratchpad/testcases/org/apache/poi/hwpf/data/TestDocument.docx diff --git a/src/ooxml/java/org/apache/poi/POIXMLDocument.java b/src/ooxml/java/org/apache/poi/POIXMLDocument.java index bcc8c0911..9fa4789db 100644 --- a/src/ooxml/java/org/apache/poi/POIXMLDocument.java +++ b/src/ooxml/java/org/apache/poi/POIXMLDocument.java @@ -32,9 +32,6 @@ import org.openxml4j.opc.PackageRelationship; import org.openxml4j.opc.PackageRelationshipCollection; import org.openxml4j.opc.PackageRelationshipTypes; import org.openxml4j.opc.PackagingURIHelper; -import org.openxml4j.opc.internal.PackagePropertiesPart; -import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.CTProperties; -import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.PropertiesDocument; public abstract class POIXMLDocument { @@ -48,6 +45,12 @@ public abstract class POIXMLDocument { /** The OPC core Package Part */ private PackagePart corePart; + /** + * The properties of the OPC package, opened as needed + */ + private POIXMLProperties properties; + + protected POIXMLDocument() {} protected POIXMLDocument(Package pkg) throws IOException { @@ -178,28 +181,13 @@ public abstract class POIXMLDocument { } /** - * Get the core document properties (core ooxml properties). - * TODO: Replace with nice usermodel wrapper - * @deprecated To be replaced with a proper user-model style view of the properties + * Get the document properties. This gives you access to the + * core ooxml properties, and the extended ooxml properties. */ - public PackagePropertiesPart getCoreProperties() throws OpenXML4JException, IOException { - PackagePart propsPart = getSinglePartByRelationType(CORE_PROPERTIES_REL_TYPE); - if(propsPart == null) { - return null; + public POIXMLProperties getProperties() throws OpenXML4JException, IOException, XmlException { + if(properties == null) { + properties = new POIXMLProperties(pkg); } - return (PackagePropertiesPart)propsPart; - } - - /** - * Get the extended document properties (extended ooxml properties) - * TODO: Replace with nice usermodel wrapper - * @deprecated To be replaced with a proper user-model style view of the properties - */ - public CTProperties getExtendedProperties() throws OpenXML4JException, XmlException, IOException { - PackagePart propsPart = getSinglePartByRelationType(EXTENDED_PROPERTIES_REL_TYPE); - - PropertiesDocument props = PropertiesDocument.Factory.parse( - propsPart.getInputStream()); - return props.getProperties(); + return properties; } } diff --git a/src/ooxml/java/org/apache/poi/POIXMLProperties.java b/src/ooxml/java/org/apache/poi/POIXMLProperties.java new file mode 100644 index 000000000..7806c9b78 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/POIXMLProperties.java @@ -0,0 +1,124 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi; + +import java.io.IOException; + +import org.apache.xmlbeans.XmlException; +import org.openxml4j.exceptions.OpenXML4JException; +import org.openxml4j.opc.Package; +import org.openxml4j.opc.PackageRelationshipCollection; +import org.openxml4j.opc.internal.PackagePropertiesPart; +import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.CTProperties; +import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.PropertiesDocument; + +/** + * Wrapper around the two different kinds of OOXML properties + * a document can have + */ +public class POIXMLProperties { + private Package pkg; + private CoreProperties core; + private ExtendedProperties ext; + + public POIXMLProperties(Package docPackage) throws IOException, OpenXML4JException, XmlException { + this.pkg = docPackage; + + // Core properties + PackageRelationshipCollection coreRel = + pkg.getRelationshipsByType(POIXMLDocument.CORE_PROPERTIES_REL_TYPE); + if(coreRel.size() == 1) { + core = new CoreProperties( (PackagePropertiesPart) + pkg.getPart(coreRel.getRelationship(0)) ); + } else { + throw new IllegalArgumentException("A document must always have core properties defined!"); + } + + // Extended properties + PackageRelationshipCollection extRel = + pkg.getRelationshipsByType(POIXMLDocument.EXTENDED_PROPERTIES_REL_TYPE); + if(extRel.size() == 1) { + PropertiesDocument props = PropertiesDocument.Factory.parse( + pkg.getPart( extRel.getRelationship(0) ).getInputStream() + ); + ext = new ExtendedProperties(props); + } else { + ext = new ExtendedProperties(PropertiesDocument.Factory.newInstance()); + } + } + + /** + * Returns the core document properties + */ + public CoreProperties getCoreProperties() { + return core; + } + + /** + * Returns the extended document properties + */ + public ExtendedProperties getExtendedProperties() { + return ext; + } + + /** + * Writes out the ooxml properties into the supplied, + * new Package + */ + public void write(Package pkg) { + // TODO + } + + /** + * The core document properties + */ + public class CoreProperties { + private PackagePropertiesPart part; + private CoreProperties(PackagePropertiesPart part) { + this.part = part; + } + + public void setTitle(String title) { + part.setTitleProperty(title); + } + public String getTitle() { + return part.getTitleProperty().getValue(); + } + + public PackagePropertiesPart getUnderlyingProperties() { + return part; + } + } + + /** + * Extended document properties + */ + public class ExtendedProperties { + private PropertiesDocument props; + private ExtendedProperties(PropertiesDocument props) { + this.props = props; + + if(props.getProperties() == null) { + props.addNewProperties(); + } + } + + public CTProperties getUnderlyingProperties() { + return props.getProperties(); + } + } +} diff --git a/src/ooxml/java/org/apache/poi/POIXMLTextExtractor.java b/src/ooxml/java/org/apache/poi/POIXMLTextExtractor.java index c28eba49d..ae8514c27 100644 --- a/src/ooxml/java/org/apache/poi/POIXMLTextExtractor.java +++ b/src/ooxml/java/org/apache/poi/POIXMLTextExtractor.java @@ -16,6 +16,12 @@ ==================================================================== */ package org.apache.poi; +import java.io.IOException; + +import org.apache.poi.POIXMLProperties.*; +import org.apache.xmlbeans.XmlException; +import org.openxml4j.exceptions.OpenXML4JException; + public abstract class POIXMLTextExtractor extends POITextExtractor { /** The POIXMLDocument that's open */ protected POIXMLDocument document; @@ -28,4 +34,17 @@ public abstract class POIXMLTextExtractor extends POITextExtractor { this.document = document; } + + /** + * Returns the core document properties + */ + public CoreProperties getCoreProperties() throws IOException, OpenXML4JException, XmlException { + return document.getProperties().getCoreProperties(); + } + /** + * Returns the extended document properties + */ + public ExtendedProperties getExtendedProperties() throws IOException, OpenXML4JException, XmlException { + return document.getProperties().getExtendedProperties(); + } } diff --git a/src/ooxml/java/org/apache/poi/xwpf/XWPFDocument.java b/src/ooxml/java/org/apache/poi/xwpf/XWPFDocument.java index 4b54ed863..05b716d75 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/XWPFDocument.java +++ b/src/ooxml/java/org/apache/poi/xwpf/XWPFDocument.java @@ -24,6 +24,7 @@ import org.openxml4j.exceptions.InvalidFormatException; import org.openxml4j.exceptions.OpenXML4JException; import org.openxml4j.opc.Package; import org.openxml4j.opc.PackagePart; +import org.openxml4j.opc.PackageRelationshipCollection; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyles; @@ -47,6 +48,7 @@ public class XWPFDocument extends POIXMLDocument { public static final String HEADER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"; public static final String STYLES_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"; public static final String STYLES_RELATION_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"; + public static final String HYPERLINK_RELATION_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"; private DocumentDocument wordDoc; @@ -89,4 +91,18 @@ public class XWPFDocument extends POIXMLDocument { StylesDocument.Factory.parse(parts[0].getInputStream()); return sd.getStyles(); } + + /** + * Returns all the hyperlink relations for the file. + * You'll generally want to get the target to get + * the destination of the hyperlink + */ + public PackageRelationshipCollection getHyperlinks() { + try { + return getCorePart().getRelationshipsByType(HYPERLINK_RELATION_TYPE); + } catch(InvalidFormatException e) { + // Should never happen + throw new IllegalStateException(e); + } + } } diff --git a/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java b/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java index 58fa4839c..bd1936d16 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java +++ b/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java @@ -16,7 +16,6 @@ ==================================================================== */ package org.apache.poi.xwpf.extractor; -import java.io.File; import java.io.IOException; import org.apache.poi.POIXMLDocument; @@ -25,7 +24,9 @@ import org.apache.poi.xwpf.XWPFDocument; import org.apache.xmlbeans.XmlException; import org.openxml4j.exceptions.OpenXML4JException; import org.openxml4j.opc.Package; +import org.openxml4j.opc.PackageRelationship; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHyperlink; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; @@ -35,6 +36,7 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; */ public class XWPFWordExtractor extends POIXMLTextExtractor { private XWPFDocument document; + private boolean fetchHyperlinks = false; public XWPFWordExtractor(Package container) throws XmlException, OpenXML4JException, IOException { this(new XWPFDocument(container)); @@ -56,6 +58,15 @@ public class XWPFWordExtractor extends POIXMLTextExtractor { )); System.out.println(extractor.getText()); } + + /** + * Should we also fetch the hyperlinks, when fetching + * the text content? Default is to only output the + * hyperlink label, and not the contents + */ + public void setFetchHyperlinks(boolean fetch) { + fetchHyperlinks = fetch; + } public String getText() { CTBody body = document.getDocumentBody(); @@ -64,9 +75,10 @@ public class XWPFWordExtractor extends POIXMLTextExtractor { // Loop over paragraphs CTP[] ps = body.getPArray(); for (int i = 0; i < ps.length; i++) { - // Loop over ranges + // Loop over ranges and hyperlinks + // TODO - properly intersperce ranges and hyperlinks CTR[] rs = ps[i].getRArray(); - for (int j = 0; j < rs.length; j++) { + for(int j = 0; j < rs.length; j++) { // Loop over text runs CTText[] texts = rs[j].getTArray(); for (int k = 0; k < texts.length; k++) { @@ -75,6 +87,26 @@ public class XWPFWordExtractor extends POIXMLTextExtractor { ); } } + + CTHyperlink[] hls = ps[i].getHyperlinkArray(); + for(CTHyperlink hl : hls) { + for(CTR r : hl.getRArray()) { + for(CTText txt : r.getTArray()) { + text.append(txt.getStringValue()); + } + } + if(fetchHyperlinks) { + String id = hl.getId(); + if(id != null) { + PackageRelationship hlRel = + document.getHyperlinks().getRelationshipByID(id); + if(hlRel != null) { + text.append(" <" + hlRel.getTargetURI().toString() + ">"); + } + } + } + } + // New line after each paragraph. text.append("\n"); } diff --git a/src/ooxml/testcases/org/apache/poi/xslf/TestXSLFSlideShow.java b/src/ooxml/testcases/org/apache/poi/xslf/TestXSLFSlideShow.java index c25d08a90..682fb9757 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/TestXSLFSlideShow.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/TestXSLFSlideShow.java @@ -46,7 +46,7 @@ public class TestXSLFSlideShow extends TestCase { if(part.getContentType().equals(XSLFSlideShow.MAIN_CONTENT_TYPE)) { found = true; } - System.out.println(part); + //System.out.println(part); } assertTrue(found); } @@ -110,14 +110,14 @@ public class TestXSLFSlideShow extends TestCase { public void testMetadataBasics() throws Exception { XSLFSlideShow xml = new XSLFSlideShow(sampleFile); - assertNotNull(xml.getCoreProperties()); - assertNotNull(xml.getExtendedProperties()); + assertNotNull(xml.getProperties().getCoreProperties()); + assertNotNull(xml.getProperties().getExtendedProperties()); - assertEquals("Microsoft Office PowerPoint", xml.getExtendedProperties().getApplication()); - assertEquals(0, xml.getExtendedProperties().getCharacters()); - assertEquals(0, xml.getExtendedProperties().getLines()); + assertEquals("Microsoft Office PowerPoint", xml.getProperties().getExtendedProperties().getUnderlyingProperties().getApplication()); + assertEquals(0, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getCharacters()); + assertEquals(0, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getLines()); - assertEquals(null, xml.getCoreProperties().getTitleProperty().getValue()); - assertEquals(null, xml.getCoreProperties().getSubjectProperty().getValue()); + assertEquals(null, xml.getProperties().getCoreProperties().getTitle()); + assertEquals(null, xml.getProperties().getCoreProperties().getUnderlyingProperties().getSubjectProperty().getValue()); } } diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFDocument.java b/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFDocument.java index 94127193f..0bd24a207 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFDocument.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFDocument.java @@ -92,29 +92,29 @@ public class TestXWPFDocument extends TestCase { XWPFDocument xml = new XWPFDocument( POIXMLDocument.openPackage(sampleFile.toString()) ); - assertNotNull(xml.getCoreProperties()); - assertNotNull(xml.getExtendedProperties()); + assertNotNull(xml.getProperties().getCoreProperties()); + assertNotNull(xml.getProperties().getExtendedProperties()); - assertEquals("Microsoft Office Word", xml.getExtendedProperties().getApplication()); - assertEquals(1315, xml.getExtendedProperties().getCharacters()); - assertEquals(10, xml.getExtendedProperties().getLines()); + assertEquals("Microsoft Office Word", xml.getProperties().getExtendedProperties().getUnderlyingProperties().getApplication()); + assertEquals(1315, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getCharacters()); + assertEquals(10, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getLines()); - assertEquals(null, xml.getCoreProperties().getTitleProperty().getValue()); - assertEquals(null, xml.getCoreProperties().getSubjectProperty().getValue()); + assertEquals(null, xml.getProperties().getCoreProperties().getTitle()); + assertEquals(null, xml.getProperties().getCoreProperties().getUnderlyingProperties().getSubjectProperty().getValue()); } public void testMetadataComplex() throws Exception { XWPFDocument xml = new XWPFDocument( POIXMLDocument.openPackage(complexFile.toString()) ); - assertNotNull(xml.getCoreProperties()); - assertNotNull(xml.getExtendedProperties()); + assertNotNull(xml.getProperties().getCoreProperties()); + assertNotNull(xml.getProperties().getExtendedProperties()); - assertEquals("Microsoft Office Outlook", xml.getExtendedProperties().getApplication()); - assertEquals(5184, xml.getExtendedProperties().getCharacters()); - assertEquals(0, xml.getExtendedProperties().getLines()); + assertEquals("Microsoft Office Outlook", xml.getProperties().getExtendedProperties().getUnderlyingProperties().getApplication()); + assertEquals(5184, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getCharacters()); + assertEquals(0, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getLines()); - assertEquals(" ", xml.getCoreProperties().getTitleProperty().getValue()); - assertEquals(" ", xml.getCoreProperties().getSubjectProperty().getValue()); + assertEquals(" ", xml.getProperties().getCoreProperties().getTitle()); + assertEquals(" ", xml.getProperties().getCoreProperties().getUnderlyingProperties().getSubjectProperty().getValue()); } } diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java b/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java index 9a1f239bc..e62dd66ce 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java @@ -37,6 +37,12 @@ public class TestXWPFWordExtractor extends TestCase { */ private XWPFDocument xmlB; private File fileB; + + /** + * File with hyperlinks + */ + private XWPFDocument xmlC; + private File fileC; protected void setUp() throws Exception { super.setUp(); @@ -49,11 +55,17 @@ public class TestXWPFWordExtractor extends TestCase { System.getProperty("HWPF.testdata.path") + File.separator + "IllustrativeCases.docx" ); + fileC = new File( + System.getProperty("HWPF.testdata.path") + + File.separator + "TestDocument.docx" + ); assertTrue(fileA.exists()); assertTrue(fileB.exists()); + assertTrue(fileC.exists()); xmlA = new XWPFDocument(POIXMLDocument.openPackage(fileA.toString())); xmlB = new XWPFDocument(POIXMLDocument.openPackage(fileB.toString())); + xmlC = new XWPFDocument(POIXMLDocument.openPackage(fileC.toString())); } /** @@ -117,4 +129,32 @@ public class TestXWPFWordExtractor extends TestCase { } assertEquals(79, ps); } + + public void testGetWithHyperlinks() throws Exception { + XWPFWordExtractor extractor = + new XWPFWordExtractor(xmlC); + extractor.getText(); + extractor.setFetchHyperlinks(true); + extractor.getText(); + + // Now check contents + // TODO - fix once correctly handling contents + extractor.setFetchHyperlinks(false); + assertEquals( +// "This is a test document\nThis bit is in bold and italic\n" + +// "Back to normal\nWe have a hyperlink here, and another.\n", + "This is a test document\nThis bit is in bold and italic\n" + + "Back to normal\nWe have a here, and .hyperlinkanother\n", + extractor.getText() + ); + + extractor.setFetchHyperlinks(true); + assertEquals( +// "This is a test document\nThis bit is in bold and italic\n" + +// "Back to normal\nWe have a hyperlink here, and another.\n", + "This is a test document\nThis bit is in bold and italic\n" + + "Back to normal\nWe have a here, and .hyperlink another\n", + extractor.getText() + ); + } } diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/data/TestDocument.docx b/src/scratchpad/testcases/org/apache/poi/hwpf/data/TestDocument.docx new file mode 100755 index 0000000000000000000000000000000000000000..058dec5e4c3dff0b7afbe5e0ad2a93a0359bbb82 GIT binary patch literal 10369 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