From ce544b85503b695eff0ceb96d67e239962bcb06c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Alain=20B=C3=A9arez?= Date: Sun, 29 Mar 2020 22:21:35 +0000 Subject: [PATCH] allow add and remove a HyperlinkRun or a FieldRun git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1875862 13f79535-47bb-0310-9956-ffa450edef68 --- .../usermodel/examples/SimpleDocument.java | 6 + .../poi/xwpf/usermodel/XWPFParagraph.java | 221 ++++++++++++++---- .../poi/xwpf/usermodel/TestXWPFParagraph.java | 132 +++++++++++ 3 files changed, 314 insertions(+), 45 deletions(-) diff --git a/src/examples/src/org/apache/poi/xwpf/usermodel/examples/SimpleDocument.java b/src/examples/src/org/apache/poi/xwpf/usermodel/examples/SimpleDocument.java index 9de12c5aa4..2699015445 100644 --- a/src/examples/src/org/apache/poi/xwpf/usermodel/examples/SimpleDocument.java +++ b/src/examples/src/org/apache/poi/xwpf/usermodel/examples/SimpleDocument.java @@ -27,6 +27,7 @@ import org.apache.poi.xwpf.usermodel.TextAlignment; import org.apache.poi.xwpf.usermodel.UnderlinePatterns; import org.apache.poi.xwpf.usermodel.VerticalAlign; import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFHyperlinkRun; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; @@ -78,6 +79,11 @@ public class SimpleDocument { r3.setFontSize(20); r3.setSubscript(VerticalAlign.SUPERSCRIPT); + // hyperlink + XWPFHyperlinkRun hyperlink = p2.insertNewHyperlinkRun(0, "http://poi.apache.org/"); + hyperlink.setUnderline(UnderlinePatterns.SINGLE); + hyperlink.setColor("0000ff"); + hyperlink.setText("Apache POI"); XWPFParagraph p3 = doc.createParagraph(); p3.setWordWrapped(true); diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java index 9b1176be6a..cdb2f786c7 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java @@ -20,6 +20,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.apache.poi.ooxml.POIXMLDocumentPart; import org.apache.poi.util.Internal; @@ -1478,50 +1479,144 @@ public class XWPFParagraph implements IBodyElement, IRunBody, ISDTContents, Para } /** - * insert a new Run in RunArray + * Appends a new field run to this paragraph + * + * @return a new field run + */ + public XWPFFieldRun createFieldRun() { + CTSimpleField ctSimpleField = paragraph.addNewFldSimple(); + XWPFFieldRun newRun = new XWPFFieldRun(ctSimpleField, ctSimpleField.addNewR(), this); + runs.add(newRun); + iruns.add(newRun); + return newRun; + } + + /** + * insert a new Run in all runs * * @param pos The position at which the new run should be added. * * @return the inserted run or null if the given pos is out of bounds. */ public XWPFRun insertNewRun(int pos) { - if (pos >= 0 && pos <= runs.size()) { - // calculate the correct pos as our run/irun list contains - // hyperlinks - // and fields so it is different to the paragraph R array. - int rPos = 0; - for (int i = 0; i < pos; i++) { - XWPFRun currRun = runs.get(i); - if (!(currRun instanceof XWPFHyperlinkRun - || currRun instanceof XWPFFieldRun)) { - rPos++; - } - } + if (pos == runs.size()) { + return createRun(); + } + return insertNewProvidedRun(pos, newCursor -> { + String uri = CTR.type.getName().getNamespaceURI(); + String localPart = "r"; + // creates a new run, cursor is positioned inside the new + // element + newCursor.beginElement(localPart, uri); + // move the cursor to the START token to the run just created + newCursor.toParent(); + CTR r = (CTR) newCursor.getObject(); + return new XWPFRun(r, (IRunBody)this); + }); + } + + /** + * insert a new hyperlink Run in all runs + * + * @param pos The position at which the new run should be added. + * @param uri hyperlink uri + * + * @return the inserted run or null if the given pos is out of bounds. + */ + public XWPFHyperlinkRun insertNewHyperlinkRun(int pos, String uri) { + if (pos == runs.size()) { + return createHyperlinkRun(uri); + } + XWPFHyperlinkRun newRun = insertNewProvidedRun(pos, newCursor -> { + String namespaceURI = CTHyperlink.type.getName().getNamespaceURI(); + String localPart = "hyperlink"; + newCursor.beginElement(localPart, namespaceURI); + // move the cursor to the START token to the hyperlink just created + newCursor.toParent(); + CTHyperlink ctHyperLink = (CTHyperlink) newCursor.getObject(); + return new XWPFHyperlinkRun(ctHyperLink, ctHyperLink.addNewR(), this); + }); - CTR ctRun = paragraph.insertNewR(rPos); - XWPFRun newRun = new XWPFRun(ctRun, (IRunBody) this); + String rId = getPart().getPackagePart().addExternalRelationship( + uri, XWPFRelation.HYPERLINK.getRelation() + ).getId(); + newRun.getCTHyperlink().setId(rId); + return newRun; + } + + /** + * insert a new field Run in all runs + * + * @param pos The position at which the new run should be added. + * + * @return the inserted run or null if the given pos is out of bounds. + */ + public XWPFFieldRun insertNewFieldRun(int pos) { + if (pos == runs.size()) { + return createFieldRun(); + } + return insertNewProvidedRun(pos, newCursor -> { + String uri = CTSimpleField.type.getName().getNamespaceURI(); + String localPart = "fldSimple"; + newCursor.beginElement(localPart, uri); + // move the cursor to the START token to the field just created + newCursor.toParent(); + CTSimpleField ctSimpleField = (CTSimpleField) newCursor.getObject(); + return new XWPFFieldRun(ctSimpleField, ctSimpleField.addNewR(), this); + }); + } - // To update the iruns, find where we're going - // in the normal runs, and go in there - int iPos = iruns.size(); - if (pos < runs.size()) { - XWPFRun oldAtPos = runs.get(pos); - int oldAt = iruns.indexOf(oldAtPos); + /** + * insert a new run provided by in all runs + * + * @param XWPFRun or XWPFHyperlinkRun or XWPFFieldRun + * @param pos The position at which the new run should be added. + * @param provider provide a new run at position of the given cursor. + * @return the inserted run or null if the given pos is out of bounds. + */ + private T insertNewProvidedRun(int pos, Function provider) { + if (pos >= 0 && pos < runs.size()) { + XWPFRun run = runs.get(pos); + CTR ctr = run.getCTR(); + XmlCursor newCursor = ctr.newCursor(); + if (!isCursorInParagraph(newCursor)) { + // look up correct position for CTP -> XXX -> R array + newCursor.toParent(); + } + if (isCursorInParagraph(newCursor)) { + // provide a new run + T newRun = provider.apply(newCursor); + + // To update the iruns, find where we're going + // in the normal runs, and go in there + int iPos = iruns.size(); + int oldAt = iruns.indexOf(run); if (oldAt != -1) { iPos = oldAt; } + iruns.add(iPos, newRun); + // Runs itself is easy to update + runs.add(pos, newRun); + return newRun; } - iruns.add(iPos, newRun); - - // Runs itself is easy to update - runs.add(pos, newRun); - - return newRun; + newCursor.dispose(); } - return null; } - // TODO Add methods to allow adding a HyperlinkRun or a FieldRun + + /** + * verifies that cursor is on the right position + * + * @param cursor + * @return + */ + private boolean isCursorInParagraph(XmlCursor cursor) { + XmlCursor verify = cursor.newCursor(); + verify.toParent(); + boolean result = (verify.getObject() == this.paragraph); + verify.dispose(); + return result; + } /** * this methods parse the paragraph and search for the string searched. @@ -1643,31 +1738,67 @@ public class XWPFParagraph implements IBodyElement, IRunBody, ISDTContents, Para */ public boolean removeRun(int pos) { if (pos >= 0 && pos < runs.size()) { - // Remove the run from our high level lists XWPFRun run = runs.get(pos); - if (run instanceof XWPFHyperlinkRun || - run instanceof XWPFFieldRun) { - // TODO Add support for removing these kinds of nested runs, - // which aren't on the CTP -> R array, but CTP -> XXX -> R array - throw new IllegalArgumentException("Removing Field or Hyperlink runs not yet supported"); + // CTP -> CTHyperlink -> R array + if (run instanceof XWPFHyperlinkRun + && isTheOnlyCTHyperlinkInRuns((XWPFHyperlinkRun) run)) { + XmlCursor c = ((XWPFHyperlinkRun) run).getCTHyperlink() + .newCursor(); + c.removeXml(); + c.dispose(); + runs.remove(pos); + iruns.remove(run); + return true; } + // CTP -> CTField -> R array + if (run instanceof XWPFFieldRun + && isTheOnlyCTFieldInRuns((XWPFFieldRun) run)) { + XmlCursor c = ((XWPFFieldRun) run).getCTField().newCursor(); + c.removeXml(); + c.dispose(); + runs.remove(pos); + iruns.remove(run); + return true; + } + XmlCursor c = run.getCTR().newCursor(); + c.removeXml(); + c.dispose(); runs.remove(pos); iruns.remove(run); - // Remove the run from the low-level XML - //calculate the correct pos as our run/irun list contains hyperlinks and fields so is different to the paragraph R array. - int rPos = 0; - for(int i=0;i (r instanceof XWPFHyperlinkRun + && ctHyperlink == ((XWPFHyperlinkRun) r).getCTHyperlink())) + .count(); + return count <= 1; + } + + /** + * Is there only one ctField in all runs + * + * @param run + * field run + * @return + */ + private boolean isTheOnlyCTFieldInRuns(XWPFFieldRun run) { + CTSimpleField ctField = run.getCTField(); + long count = runs.stream().filter(r -> (r instanceof XWPFFieldRun + && ctField == ((XWPFFieldRun) r).getCTField())).count(); + return count <= 1; + } + /** * returns the type of the BodyElement Paragraph * diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFParagraph.java b/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFParagraph.java index eae2754527..3ce354b089 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFParagraph.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFParagraph.java @@ -327,6 +327,138 @@ public final class TestXWPFParagraph { } } + @Test + public void testCreateNewRuns() throws IOException { + try (XWPFDocument doc = new XWPFDocument()) { + + XWPFParagraph p = doc.createParagraph(); + XWPFHyperlinkRun h = p.createHyperlinkRun("http://poi.apache.org"); + XWPFFieldRun fieldRun = p.createFieldRun(); + XWPFRun r = p.createRun(); + + assertEquals(3, p.getRuns().size()); + assertEquals(0, p.getRuns().indexOf(h)); + assertEquals(1, p.getRuns().indexOf(fieldRun)); + assertEquals(2, p.getRuns().indexOf(r)); + + assertEquals(3, p.getIRuns().size()); + assertEquals(0, p.getIRuns().indexOf(h)); + assertEquals(1, p.getIRuns().indexOf(fieldRun)); + assertEquals(2, p.getIRuns().indexOf(r)); + } + } + + @Test + public void testInsertNewRuns() throws IOException { + try (XWPFDocument doc = new XWPFDocument()) { + + XWPFParagraph p = doc.createParagraph(); + XWPFRun r = p.createRun(); + assertEquals(1, p.getRuns().size()); + assertEquals(0, p.getRuns().indexOf(r)); + + XWPFHyperlinkRun h = p.insertNewHyperlinkRun(0, "http://poi.apache.org"); + assertEquals(2, p.getRuns().size()); + assertEquals(0, p.getRuns().indexOf(h)); + assertEquals(1, p.getRuns().indexOf(r)); + + XWPFFieldRun fieldRun2 = p.insertNewFieldRun(2); + assertEquals(3, p.getRuns().size()); + assertEquals(2, p.getRuns().indexOf(fieldRun2)); + } + } + + @Test + public void testRemoveRuns() throws IOException { + try (XWPFDocument doc = new XWPFDocument()) { + + XWPFParagraph p = doc.createParagraph(); + XWPFRun r = p.createRun(); + p.createRun(); + XWPFHyperlinkRun hyperlinkRun = p + .createHyperlinkRun("http://poi.apache.org"); + XWPFFieldRun fieldRun = p.createFieldRun(); + + assertEquals(4, p.getRuns().size()); + assertEquals(2, p.getRuns().indexOf(hyperlinkRun)); + assertEquals(3, p.getRuns().indexOf(fieldRun)); + + p.removeRun(2); + assertEquals(3, p.getRuns().size()); + assertEquals(-1, p.getRuns().indexOf(hyperlinkRun)); + assertEquals(2, p.getRuns().indexOf(fieldRun)); + + p.removeRun(0); + assertEquals(2, p.getRuns().size()); + assertEquals(-1, p.getRuns().indexOf(r)); + assertEquals(1, p.getRuns().indexOf(fieldRun)); + + p.removeRun(1); + assertEquals(1, p.getRuns().size()); + assertEquals(-1, p.getRuns().indexOf(fieldRun)); + } + } + + @Test + public void testRemoveAndInsertRunsWithOtherIRunElement() + throws IOException { + XWPFDocument doc = new XWPFDocument(); + + XWPFParagraph p = doc.createParagraph(); + p.createRun(); + // add other run element + p.getCTP().addNewSdt(); + // add two CTR in hyperlink + XWPFHyperlinkRun hyperlinkRun = p + .createHyperlinkRun("http://poi.apache.org"); + hyperlinkRun.getCTHyperlink().addNewR(); + p.createFieldRun(); + + XWPFDocument doc2 = XWPFTestDataSamples.writeOutAndReadBack(doc); + XWPFParagraph paragraph = doc2.getParagraphArray(0); + + assertEquals(4, paragraph.getRuns().size()); + assertEquals(5, paragraph.getIRuns().size()); + + assertTrue(paragraph.getRuns().get(1) instanceof XWPFHyperlinkRun); + assertTrue(paragraph.getRuns().get(2) instanceof XWPFHyperlinkRun); + assertTrue(paragraph.getRuns().get(3) instanceof XWPFFieldRun); + + assertTrue(paragraph.getIRuns().get(1) instanceof XWPFSDT); + assertTrue(paragraph.getIRuns().get(2) instanceof XWPFHyperlinkRun); + + paragraph.removeRun(1); + assertEquals(3, paragraph.getRuns().size()); + assertTrue(paragraph.getRuns().get(1) instanceof XWPFHyperlinkRun); + assertTrue(paragraph.getRuns().get(2) instanceof XWPFFieldRun); + + assertTrue(paragraph.getIRuns().get(1) instanceof XWPFSDT); + assertTrue(paragraph.getIRuns().get(2) instanceof XWPFHyperlinkRun); + + paragraph.removeRun(1); + assertEquals(2, paragraph.getRuns().size()); + assertTrue(paragraph.getRuns().get(1) instanceof XWPFFieldRun); + + assertTrue(paragraph.getIRuns().get(1) instanceof XWPFSDT); + assertTrue(paragraph.getIRuns().get(2) instanceof XWPFFieldRun); + + paragraph.removeRun(0); + assertEquals(1, paragraph.getRuns().size()); + assertTrue(paragraph.getRuns().get(0) instanceof XWPFFieldRun); + + assertTrue(paragraph.getIRuns().get(0) instanceof XWPFSDT); + assertTrue(paragraph.getIRuns().get(1) instanceof XWPFFieldRun); + + XWPFRun newRun = paragraph.insertNewRun(0); + assertEquals(2, paragraph.getRuns().size()); + + assertEquals(3, paragraph.getIRuns().size()); + assertEquals(0, paragraph.getRuns().indexOf(newRun)); + + doc.close(); + doc2.close(); + } + @Test public void testPictures() throws IOException { try (XWPFDocument doc = XWPFTestDataSamples.openSampleDocument("VariousPictures.docx")) { -- 2.39.5