[bug-63189] support hyperlink relationships. Thanks to Ohyoung Kwon. This closes #617

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1917134 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
PJ Fanning 2024-04-19 09:23:54 +00:00
parent d525d1a5b1
commit eebb3717e0
12 changed files with 328 additions and 46 deletions

View File

@ -0,0 +1,44 @@
/* ====================================================================
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.ooxml;
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;
import java.net.URI;
/**
* Represents a hyperlink relationship.
*
* @since POI 5.2.6
*/
public class HyperlinkRelationship extends ReferenceRelationship {
/**
* Initializes a new instance of the HyperlinkRelationship.
*
* @param hyperlinkUri The target uri of the hyperlink relationship.
* @param isExternal Is the URI external.
* @param id The relationship ID.
*/
protected HyperlinkRelationship(POIXMLDocumentPart container, URI hyperlinkUri, boolean isExternal, String id) {
super(container, hyperlinkUri, isExternal, PackageRelationshipTypes.HYPERLINK_PART, id);
}
@Override
public String getRelationshipType() {
return PackageRelationshipTypes.HYPERLINK_PART;
}
}

View File

@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
@ -38,7 +39,6 @@ import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.openxml4j.opc.TargetMode;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;
import org.apache.poi.xddf.usermodel.chart.XDDFChart;
import org.apache.poi.xssf.usermodel.XSSFRelation;
@ -59,6 +59,7 @@ public class POIXMLDocumentPart {
private PackagePart packagePart;
private POIXMLDocumentPart parent;
private final Map<String, RelationPart> relations = new LinkedHashMap<>();
private final Map<String, ReferenceRelationship> referenceRelationships = new LinkedHashMap<>();
private boolean isCommitted = false;
/**
@ -640,38 +641,42 @@ public class POIXMLDocumentPart {
// scan breadth-first, so parent-relations are hopefully the shallowest element
for (PackageRelationship rel : rels) {
if (rel.getTargetMode() == TargetMode.INTERNAL) {
URI uri = rel.getTargetURI();
if (Objects.equals(rel.getRelationshipType(), HyperlinkRelationship.HYPERLINK_REL_TYPE)) {
referenceRelationships.put(rel.getId(), new HyperlinkRelationship(this, rel.getTargetURI(), rel.getTargetMode() == TargetMode.EXTERNAL, rel.getId()));
} else {
if (rel.getTargetMode() == TargetMode.INTERNAL) {
URI uri = rel.getTargetURI();
// check for internal references (e.g. '#Sheet1!A1')
PackagePartName relName;
if (uri.getRawFragment() != null) {
relName = PackagingURIHelper.createPartName(uri.getPath());
} else {
relName = PackagingURIHelper.createPartName(uri);
}
final PackagePart p = packagePart.getPackage().getPart(relName);
if (p == null) {
LOG.atError().log("Skipped invalid entry {}", rel.getTargetURI());
continue;
}
POIXMLDocumentPart childPart = context.get(p);
if (childPart == null) {
childPart = factory.createDocumentPart(this, p);
//here we are checking if part if embedded and excel then set it to chart class
//so that at the time to writing we can also write updated embedded part
if (this instanceof XDDFChart && childPart instanceof XSSFWorkbook) {
((XDDFChart) this).setWorkbook((XSSFWorkbook) childPart);
// check for internal references (e.g. '#Sheet1!A1')
PackagePartName relName;
if (uri.getRawFragment() != null) {
relName = PackagingURIHelper.createPartName(uri.getPath());
} else {
relName = PackagingURIHelper.createPartName(uri);
}
childPart.parent = this;
// already add child to context, so other children can reference it
context.put(p, childPart);
readLater.add(childPart);
}
addRelation(rel, childPart);
final PackagePart p = packagePart.getPackage().getPart(relName);
if (p == null) {
LOG.atError().log("Skipped invalid entry {}", rel.getTargetURI());
continue;
}
POIXMLDocumentPart childPart = context.get(p);
if (childPart == null) {
childPart = factory.createDocumentPart(this, p);
//here we are checking if part if embedded and excel then set it to chart class
//so that at the time to writing we can also write updated embedded part
if (this instanceof XDDFChart && childPart instanceof XSSFWorkbook) {
((XDDFChart) this).setWorkbook((XSSFWorkbook) childPart);
}
childPart.parent = this;
// already add child to context, so other children can reference it
context.put(p, childPart);
readLater.add(childPart);
}
addRelation(rel, childPart);
}
}
}
@ -767,4 +772,31 @@ public class POIXMLDocumentPart {
throw new POIXMLException("OOXML file structure broken/invalid", e);
}
}
public boolean removeReferenceRelationship(String relId) {
ReferenceRelationship existing = referenceRelationships.remove(relId);
if (existing != null) {
packagePart.removeRelationship(relId);
return true;
}
return false;
}
public ReferenceRelationship getReferenceRelationship(String relId) {
return referenceRelationships.get(relId);
}
public HyperlinkRelationship createHyperlink(URI uri, boolean isExternal, String relId) {
PackageRelationship pr = packagePart.addRelationship(uri, isExternal ? TargetMode.EXTERNAL : TargetMode.INTERNAL,
HyperlinkRelationship.HYPERLINK_REL_TYPE, relId);
HyperlinkRelationship hyperlink = new HyperlinkRelationship(this, uri, isExternal, relId);
referenceRelationships.put(relId, hyperlink);
return hyperlink;
}
public List<ReferenceRelationship> getReferenceRelationships() {
List<ReferenceRelationship> list = new ArrayList<>(referenceRelationships.values());
return Collections.unmodifiableList(list);
}
}

View File

@ -0,0 +1,79 @@
/* ====================================================================
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.ooxml;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.TargetMode;
import java.net.URI;
/**
* Defines a reference relationship. A reference relationship can be internal or external.
*
* @since POI 5.2.6
*/
public abstract class ReferenceRelationship {
private POIXMLDocumentPart container;
private final String relationshipType;
private final boolean external;
private final String id;
private final URI uri;
protected ReferenceRelationship(POIXMLDocumentPart container, PackageRelationship packageRelationship) {
if (packageRelationship == null) {
throw new IllegalArgumentException("packageRelationship");
}
this.container = container;
this.relationshipType = packageRelationship.getRelationshipType();
this.uri = packageRelationship.getTargetURI();
this.external = packageRelationship.getTargetMode() == TargetMode.EXTERNAL;
this.id = packageRelationship.getId();
}
protected ReferenceRelationship(POIXMLDocumentPart container, URI targetUri, boolean isExternal, String relationshipType, String id) {
if (targetUri == null) {
throw new IllegalArgumentException("targetUri");
}
this.container = container;
this.relationshipType = relationshipType;
this.uri = targetUri;
this.id = id;
this.external = isExternal;
}
public POIXMLDocumentPart getContainer() {
return container;
}
public String getRelationshipType() {
return relationshipType;
}
public boolean isExternal() {
return external;
}
public String getId() {
return id;
}
public URI getUri() {
return uri;
}
}

View File

@ -186,6 +186,12 @@ public final class PackageRelationship {
return targetUri;
}
// If it's an internal hyperlink target, we don't
// need to apply our normal validation rules
if (PackageRelationshipTypes.HYPERLINK_PART.equals(relationshipType)) {
return targetUri;
}
// Internal target
// If it isn't absolute, resolve it relative
// to ourselves

View File

@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Objects;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
@ -32,6 +33,7 @@ import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.openxml4j.opc.StreamHelper;
import org.apache.poi.openxml4j.opc.TargetMode;
@ -154,7 +156,14 @@ public final class ZipPartMarshaller implements PartMarshaller {
// the relationship Target
String targetValue;
URI uri = rel.getTargetURI();
if (rel.getTargetMode() == TargetMode.EXTERNAL) {
if (Objects.equals(rel.getRelationshipType(), PackageRelationshipTypes.HYPERLINK_PART)) {
// Save the target as-is - we don't need to validate it,
targetValue = uri.toString();
if (rel.getTargetMode() == TargetMode.EXTERNAL) {
// add TargetMode attribute (as it is external link external)
relElem.setAttribute(PackageRelationship.TARGET_MODE_ATTRIBUTE_NAME, "External");
}
} else if (rel.getTargetMode() == TargetMode.EXTERNAL) {
// Save the target as-is - we don't need to validate it,
// alter it etc
targetValue = uri.toString();

View File

@ -47,10 +47,12 @@ import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.hpsf.ClassIDPredefined;
import org.apache.poi.ooxml.HyperlinkRelationship;
import org.apache.poi.ooxml.POIXMLDocument;
import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.ooxml.POIXMLException;
import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.ooxml.ReferenceRelationship;
import org.apache.poi.ooxml.util.PackageHelper;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
@ -683,6 +685,14 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook, Date1904Su
addRelation(rp, clonedSheet);
}
// copy sheet's reference relations;
List<ReferenceRelationship> referenceRelationships = srcSheet.getReferenceRelationships();
for (ReferenceRelationship ref : referenceRelationships) {
if (ref instanceof HyperlinkRelationship) {
createHyperlink(ref.getUri(), ref.isExternal(), ref.getId());
}
}
try {
for(PackageRelationship pr : srcSheet.getPackagePart().getRelationships()) {
if (pr.getTargetMode() == TargetMode.EXTERNAL) {
@ -742,6 +752,14 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook, Date1904Su
addRelation(rp, clonedDg);
}
}
// copy sheet's reference relations;
List<ReferenceRelationship> srcRefs = drawingPatriarch.getReferenceRelationships();
for (ReferenceRelationship ref : srcRefs) {
if (ref instanceof HyperlinkRelationship) {
clonedDg.createHyperlink(ref.getUri(), ref.isExternal(), ref.getId());
}
}
}
}
XSSFSheet.cloneTables(clonedSheet);

View File

@ -284,7 +284,7 @@ public final class TestPackage {
assertEquals(1, rels.size());
PackageRelationship rel = rels.getRelationship(0);
assertNotNull(rel);
assertEquals("Sheet1!A1", rel.getTargetURI().getRawFragment());
assertEquals("#Sheet1!A1", rel.getTargetURI().toString());
assertMSCompatibility(pkg);
}

View File

@ -323,10 +323,11 @@ class TestRelationships {
PackageRelationship rId1 = drawingPart.getRelationship("rId1");
URI parent = drawingPart.getPartName().getURI();
URI rel1 = parent.relativize(rId1.getTargetURI());
URI rel11 = PackagingURIHelper.relativizeURI(drawingPart.getPartName().getURI(), rId1.getTargetURI());
assertEquals("'Another Sheet'!A1", rel1.getFragment());
assertEquals("'Another Sheet'!A1", rel11.getFragment());
// Hyperlink is not a target of relativize() because it is not resolved based on sourceURI in getTargetURI()
// URI rel1 = parent.relativize(rId1.getTargetURI());
// URI rel11 = PackagingURIHelper.relativizeURI(drawingPart.getPartName().getURI(), rId1.getTargetURI());
// assertEquals("'Another Sheet'!A1", rel1.getFragment());
// assertEquals("'Another Sheet'!A1", rel11.getFragment());
PackageRelationship rId2 = drawingPart.getRelationship("rId2");
URI rel2 = PackagingURIHelper.relativizeURI(drawingPart.getPartName().getURI(), rId2.getTargetURI());

View File

@ -53,8 +53,10 @@ import org.apache.commons.io.output.NullPrintStream;
import org.apache.poi.POIDataSamples;
import org.apache.poi.common.usermodel.HyperlinkType;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.ooxml.HyperlinkRelationship;
import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.ooxml.POIXMLDocumentPart.RelationPart;
import org.apache.poi.ooxml.ReferenceRelationship;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePartName;
@ -384,23 +386,31 @@ class TestXSLFBugs {
// Check the relations from this
Collection<RelationPart> rels = slide.getRelationParts();
Collection<ReferenceRelationship> referenceRelationships = slide.getReferenceRelationships();
// Should have 6 relations:
// 1 external hyperlink (skipped from list)
// 4 internal hyperlinks
// 1 slide layout
assertEquals(5, rels.size());
assertEquals(1, rels.size());
assertEquals(5, referenceRelationships.size());
int layouts = 0;
int hyperlinks = 0;
int extHyperLinks = 0;
for (RelationPart p : rels) {
if (p.getRelationship().getRelationshipType().equals(XSLFRelation.HYPERLINK.getRelation())) {
hyperlinks++;
} else if (p.getDocumentPart() instanceof XSLFSlideLayout) {
if (p.getDocumentPart() instanceof XSLFSlideLayout) {
layouts++;
}
}
for (ReferenceRelationship ref : referenceRelationships) {
if (ref instanceof HyperlinkRelationship) {
if (ref.isExternal()) extHyperLinks++;
else hyperlinks++;
}
}
assertEquals(1, layouts);
assertEquals(4, hyperlinks);
assertEquals(1, extHyperLinks);
// Hyperlinks should all be to #_ftn1 or #ftnref1
for (RelationPart p : rels) {

View File

@ -18,19 +18,31 @@
package org.apache.poi.xssf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.poi.ooxml.ReferenceRelationship;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.ss.usermodel.BaseTestCloneSheet;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xddf.usermodel.chart.XDDFDataSource;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFPicture;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openxmlformats.schemas.drawingml.x2006.main.CTBlip;
import org.openxmlformats.schemas.drawingml.x2006.main.CTBlipFillProperties;
import org.openxmlformats.schemas.drawingml.x2006.main.CTHyperlink;
import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps;
import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualPictureProperties;
import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTPicture;
import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTPictureNonVisual;
import java.io.IOException;
@ -127,4 +139,74 @@ class TestXSSFCloneSheet extends BaseTestCloneSheet {
}
}
@Test
void testBug63189() throws IOException {
try (XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("bug63189.xlsx")) {
// given
String linkRelationType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
String linkTargetUrl = "#Sheet3!A1";
String imageRelationType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
String imageTargetUrl = "/xl/media/image1.png";
XSSFSheet srcSheet = workbook.getSheetAt(0);
assertEquals("CloneMe", srcSheet.getSheetName());
XSSFDrawing drawing = srcSheet.getDrawingPatriarch();
assertNotNull(drawing);
assertEquals(1, drawing.getShapes().size());
assertInstanceOf(XSSFPicture.class, drawing.getShapes().get(0));
XSSFPicture lPic = (XSSFPicture)drawing.getShapes().get(0);
CTPicture pic = lPic.getCTPicture();
CTPictureNonVisual nvPicPr = pic.getNvPicPr();
CTNonVisualDrawingProps cNvPr = nvPicPr.getCNvPr();
assertTrue(cNvPr.isSetHlinkClick());
CTHyperlink hlinkClick = cNvPr.getHlinkClick();
String linkRelId = hlinkClick.getId();
ReferenceRelationship linkRel = drawing.getReferenceRelationship(linkRelId);
assertEquals(linkRelationType, linkRel.getRelationshipType());
assertEquals(linkTargetUrl, linkRel.getUri().toString());
CTNonVisualPictureProperties cNvPicPr = nvPicPr.getCNvPicPr();
assertTrue(cNvPicPr.getPicLocks().getNoChangeAspect());
CTBlipFillProperties blipFill = pic.getBlipFill();
CTBlip blip = blipFill.getBlip();
String imageRelId = blip.getEmbed();
PackageRelationship imageRel = drawing.getRelationPartById(imageRelId).getRelationship();
assertEquals(imageRelationType, imageRel.getRelationshipType());
assertEquals(imageTargetUrl, imageRel.getTargetURI().toString());
// when
XSSFSheet clonedSheet = workbook.cloneSheet(0);
// then
XSSFDrawing drawing2 = clonedSheet.getDrawingPatriarch();
assertNotNull(drawing2);
assertEquals(1, drawing2.getShapes().size());
assertInstanceOf(XSSFPicture.class, drawing2.getShapes().get(0));
XSSFPicture lPic2 = (XSSFPicture)drawing2.getShapes().get(0);
CTPicture pic2 = lPic2.getCTPicture();
CTPictureNonVisual nvPicPr2 = pic2.getNvPicPr();
CTNonVisualDrawingProps cNvPr2 = nvPicPr2.getCNvPr();
assertTrue(cNvPr2.isSetHlinkClick());
CTHyperlink hlinkClick2 = cNvPr2.getHlinkClick();
String linkRelId2 = hlinkClick2.getId();
ReferenceRelationship linkRel2 = drawing2.getReferenceRelationship(linkRelId2);
assertEquals(linkRelationType, linkRel2.getRelationshipType());
assertEquals(linkTargetUrl, linkRel2.getUri().toString());
CTNonVisualPictureProperties cNvPicPr2 = nvPicPr2.getCNvPicPr();
assertTrue(cNvPicPr2.getPicLocks().getNoChangeAspect());
CTBlipFillProperties blipFill2 = pic2.getBlipFill();
CTBlip blip2 = blipFill2.getBlip();
String imageRelId2 = blip2.getEmbed();
PackageRelationship imageRel2 = drawing2.getRelationPartById(imageRelId2).getRelationship();
assertEquals(imageRelationType, imageRel2.getRelationshipType());
assertEquals(imageTargetUrl, imageRel2.getTargetURI().toString());
}
}
}

View File

@ -60,6 +60,7 @@ import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.ooxml.POIXMLDocumentPart.RelationPart;
import org.apache.poi.ooxml.POIXMLException;
import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.ooxml.ReferenceRelationship;
import org.apache.poi.ooxml.util.DocumentHelper;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.InvalidOperationException;
@ -234,18 +235,18 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues {
assertEquals(1, wb1.getNumberOfSheets());
XSSFSheet sh = wb1.getSheetAt(0);
XSSFDrawing drawing = sh.createDrawingPatriarch();
List<RelationPart> rels = drawing.getRelationParts();
assertEquals(1, rels.size());
assertEquals("Sheet1!A1", rels.get(0).getRelationship().getTargetURI().getFragment());
List<ReferenceRelationship> referenceRelationships = drawing.getReferenceRelationships();
assertEquals(1, referenceRelationships.size());
assertEquals("#Sheet1!A1", referenceRelationships.get(0).getUri().toString());
// And again, just to be sure
try (XSSFWorkbook wb2 = writeOutAndReadBack(wb1)) {
assertEquals(1, wb2.getNumberOfSheets());
sh = wb2.getSheetAt(0);
drawing = sh.createDrawingPatriarch();
rels = drawing.getRelationParts();
assertEquals(1, rels.size());
assertEquals("Sheet1!A1", rels.get(0).getRelationship().getTargetURI().getFragment());
referenceRelationships = drawing.getReferenceRelationships();
assertEquals(1, referenceRelationships.size());
assertEquals("#Sheet1!A1", referenceRelationships.get(0).getUri().toString());
}
}
}

Binary file not shown.