From 8b2f2bcff633d330eef903ab9babe7129ed1d96e Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Mon, 20 Sep 2010 14:26:49 +0000 Subject: [PATCH] Work inspired by bug #48018 - get HWPF lists more consistent in read vs write, and preserve order as apparently that matters. Includes a fair number of list related unit tests, but not for everything git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@998943 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../org/apache/poi/hwpf/model/ListLevel.java | 6 +- .../org/apache/poi/hwpf/model/ListTables.java | 124 ++++++++--- .../apache/poi/hwpf/usermodel/Paragraph.java | 14 ++ .../apache/poi/hwpf/usermodel/TestLists.java | 210 ++++++++++++++++++ test-data/document/Lists.doc | Bin 0 -> 27648 bytes 6 files changed, 322 insertions(+), 33 deletions(-) create mode 100644 src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestLists.java create mode 100644 test-data/document/Lists.doc diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index f1b71436e1..d3eaf3f08e 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 48018 - Improve HWPF handling of lists in documents read and then saved, by preserving order better 49820 - Fix HWPF paragraph levels, so that outline levels can be properly fetched 47271 - Avoid infinite loops on broken HWPF documents with a corrupt CHP style with a parent of itself 49936 - Handle HWPF documents with problematic HeaderStories better diff --git a/src/scratchpad/src/org/apache/poi/hwpf/model/ListLevel.java b/src/scratchpad/src/org/apache/poi/hwpf/model/ListLevel.java index de8e7afccd..d6392af776 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/model/ListLevel.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/model/ListLevel.java @@ -91,7 +91,7 @@ public final class ListLevel System.arraycopy(buf, offset, _rgbxchNums, 0, RGBXCH_NUMS_SIZE); offset += RGBXCH_NUMS_SIZE; - _ixchFollow = buf[offset++]; + _ixchFollow = buf[offset++]; _dxaSpace = LittleEndian.getInt(buf, offset); offset += LittleEndian.INT_SIZE; _dxaIndent = LittleEndian.getInt(buf, offset); @@ -216,10 +216,10 @@ public final class ListLevel LittleEndian.putShort(buf, offset, _reserved); offset += LittleEndian.SHORT_SIZE; - System.arraycopy(_grpprlChpx, 0, buf, offset, _cbGrpprlChpx); - offset += _cbGrpprlChpx; System.arraycopy(_grpprlPapx, 0, buf, offset, _cbGrpprlPapx); offset += _cbGrpprlPapx; + System.arraycopy(_grpprlChpx, 0, buf, offset, _cbGrpprlChpx); + offset += _cbGrpprlChpx; if (_numberText == null) { // TODO - write junit to test this flow diff --git a/src/scratchpad/src/org/apache/poi/hwpf/model/ListTables.java b/src/scratchpad/src/org/apache/poi/hwpf/model/ListTables.java index fd72d42270..9124c1fb20 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/model/ListTables.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/model/ListTables.java @@ -17,20 +17,23 @@ package org.apache.poi.hwpf.model; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.apache.poi.hwpf.model.io.HWPFOutputStream; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; -import org.apache.poi.hwpf.model.io.*; - -import java.util.HashMap; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.NoSuchElementException; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - /** * @author Ryan Ackley */ @@ -40,8 +43,8 @@ public final class ListTables private static final int LIST_FORMAT_OVERRIDE_SIZE = 16; private static POILogger log = POILogFactory.getLogger(ListTables.class); - HashMap _listMap = new HashMap(); - ArrayList _overrideList = new ArrayList(); + ListMap _listMap = new ListMap(); + ArrayList _overrideList = new ArrayList(); public ListTables() { @@ -109,22 +112,17 @@ public final class ListTables public void writeListDataTo(HWPFOutputStream tableStream) throws IOException { - - Integer[] intList = (Integer[])_listMap.keySet().toArray(new Integer[0]); + int listSize = _listMap.size(); // use this stream as a buffer for the levels since their size varies. ByteArrayOutputStream levelBuf = new ByteArrayOutputStream(); - // use a byte array for the lists because we know their size. - byte[] listBuf = new byte[intList.length * LIST_DATA_SIZE]; - byte[] shortHolder = new byte[2]; - LittleEndian.putShort(shortHolder, (short)intList.length); + LittleEndian.putShort(shortHolder, (short)listSize); tableStream.write(shortHolder); - for (int x = 0; x < intList.length; x++) - { - ListData lst = (ListData)_listMap.get(intList[x]); + for(Integer x : _listMap.sortedKeys()) { + ListData lst = _listMap.get(x); tableStream.write(lst.toByteArray()); ListLevel[] lvls = lst.getLevels(); for (int y = 0; y < lvls.length; y++) @@ -150,7 +148,7 @@ public final class ListTables for (int x = 0; x < size; x++) { - ListFormatOverride lfo = (ListFormatOverride)_overrideList.get(x); + ListFormatOverride lfo = _overrideList.get(x); tableStream.write(lfo.toByteArray()); ListFormatOverrideLevel[] lfolvls = lfo.getLevelOverrides(); for (int y = 0; y < lfolvls.length; y++) @@ -164,7 +162,7 @@ public final class ListTables public ListFormatOverride getOverride(int lfoIndex) { - return (ListFormatOverride)_overrideList.get(lfoIndex - 1); + return _overrideList.get(lfoIndex - 1); } public int getOverrideIndexFromListID(int lstid) @@ -173,7 +171,7 @@ public final class ListTables int size = _overrideList.size(); for (int x = 0; x < size; x++) { - ListFormatOverride next = (ListFormatOverride)_overrideList.get(x); + ListFormatOverride next = _overrideList.get(x); if (next.getLsid() == lstid) { // 1-based index I think @@ -190,7 +188,7 @@ public final class ListTables public ListLevel getLevel(int listID, int level) { - ListData lst = (ListData)_listMap.get(Integer.valueOf(listID)); + ListData lst = _listMap.get(Integer.valueOf(listID)); if(level < lst.numLevels()) { ListLevel lvl = lst.getLevels()[level]; return lvl; @@ -201,7 +199,7 @@ public final class ListTables public ListData getListData(int listID) { - return (ListData) _listMap.get(Integer.valueOf(listID)); + return _listMap.get(Integer.valueOf(listID)); } public boolean equals(Object obj) @@ -215,12 +213,12 @@ public final class ListTables if (_listMap.size() == tables._listMap.size()) { - Iterator it = _listMap.keySet().iterator(); + Iterator it = _listMap.keySet().iterator(); while (it.hasNext()) { - Object key = it.next(); - ListData lst1 = (ListData)_listMap.get(key); - ListData lst2 = (ListData)tables._listMap.get(key); + Integer key = it.next(); + ListData lst1 = _listMap.get(key); + ListData lst2 = tables._listMap.get(key); if (!lst1.equals(lst2)) { return false; @@ -241,4 +239,70 @@ public final class ListTables } return false; } + + private static class ListMap implements Map { + private ArrayList keyList = new ArrayList(); + private HashMap parent = new HashMap(); + private ListMap() {} + + public void clear() { + keyList.clear(); + parent.clear(); + } + + public boolean containsKey(Object key) { + return parent.containsKey(key); + } + + public boolean containsValue(Object value) { + return parent.containsValue(value); + } + + public ListData get(Object key) { + return parent.get(key); + } + + public boolean isEmpty() { + return parent.isEmpty(); + } + + public ListData put(Integer key, ListData value) { + keyList.add(key); + return parent.put(key, value); + } + + public void putAll(Map map) { + for(Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + public ListData remove(Object key) { + keyList.remove(key); + return parent.remove(key); + } + + public int size() { + return parent.size(); + } + + public Set> entrySet() { + throw new IllegalStateException("Use sortedKeys() + get() instead"); + } + + public List sortedKeys() { + return Collections.unmodifiableList(keyList); + } + public Set keySet() { + throw new IllegalStateException("Use sortedKeys() instead"); + } + + public Collection values() { + ArrayList values = new ArrayList(); + for(Integer key : keyList) { + values.add(parent.get(key)); + } + return values; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Paragraph.java b/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Paragraph.java index 87fe4d7f1d..f51576df9f 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Paragraph.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Paragraph.java @@ -434,16 +434,30 @@ public class Paragraph extends Range implements Cloneable { _papx.updateSprm(SPRM_DCS, dcs.toShort()); } + /** + * Returns the ilfo, an index to the document's hpllfo, which + * describes the automatic number formatting of the paragraph. + * A value of zero means it isn't numbered. + */ public int getIlfo() { return _props.getIlfo(); } + /** + * Returns the multi-level indent for the paragraph. Will be + * zero for non-list paragraphs, and the first level of any + * list. Subsequent levels in hold values 1-8. + */ public int getIlvl() { return _props.getIlvl(); } + /** + * Returns the heading level (1-8), or 9 if the paragraph + * isn't in a heading style. + */ public int getLvl() { return _props.getLvl(); diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestLists.java b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestLists.java new file mode 100644 index 0000000000..eafb18fb95 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestLists.java @@ -0,0 +1,210 @@ +/* ==================================================================== + 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.hwpf.usermodel; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.apache.poi.hwpf.HWPFDocument; +import org.apache.poi.hwpf.HWPFTestDataSamples; + +/** + * Tests for our handling of lists + */ +public final class TestLists extends TestCase { + public void testBasics() { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("Lists.doc"); + Range r = doc.getRange(); + + assertEquals(40, r.numParagraphs()); + assertEquals("Heading Level 1\r", r.getParagraph(0).text()); + assertEquals("This document has different lists in it for testing\r", r.getParagraph(1).text()); + assertEquals("The end!\r", r.getParagraph(38).text()); + assertEquals("\r", r.getParagraph(39).text()); + + assertEquals(0, r.getParagraph(0).getLvl()); + assertEquals(9, r.getParagraph(1).getLvl()); + assertEquals(9, r.getParagraph(38).getLvl()); + assertEquals(9, r.getParagraph(39).getLvl()); + } + + public void testUnorderedLists() { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("Lists.doc"); + Range r = doc.getRange(); + assertEquals(40, r.numParagraphs()); + + // Normal bullet points + assertEquals("This document has different lists in it for testing\r", r.getParagraph(1).text()); + assertEquals("Unordered list 1\r", r.getParagraph(2).text()); + assertEquals("UL 2\r", r.getParagraph(3).text()); + assertEquals("UL 3\r", r.getParagraph(4).text()); + assertEquals("Next up is an ordered list:\r", r.getParagraph(5).text()); + + assertEquals(9, r.getParagraph(1).getLvl()); + assertEquals(9, r.getParagraph(2).getLvl()); + assertEquals(9, r.getParagraph(3).getLvl()); + assertEquals(9, r.getParagraph(4).getLvl()); + assertEquals(9, r.getParagraph(5).getLvl()); + + assertEquals(0, r.getParagraph(1).getIlvl()); + assertEquals(0, r.getParagraph(2).getIlvl()); + assertEquals(0, r.getParagraph(3).getIlvl()); + assertEquals(0, r.getParagraph(4).getIlvl()); + assertEquals(0, r.getParagraph(5).getIlvl()); + + // Tick bullets + assertEquals("Now for an un-ordered list with a different bullet style:\r", r.getParagraph(9).text()); + assertEquals("Tick 1\r", r.getParagraph(10).text()); + assertEquals("Tick 2\r", r.getParagraph(11).text()); + assertEquals("Multi-level un-ordered list:\r", r.getParagraph(12).text()); + + assertEquals(9, r.getParagraph(9).getLvl()); + assertEquals(9, r.getParagraph(10).getLvl()); + assertEquals(9, r.getParagraph(11).getLvl()); + assertEquals(9, r.getParagraph(12).getLvl()); + + assertEquals(0, r.getParagraph(9).getIlvl()); + assertEquals(0, r.getParagraph(10).getIlvl()); + assertEquals(0, r.getParagraph(11).getIlvl()); + assertEquals(0, r.getParagraph(12).getIlvl()); + + // TODO Test for tick not bullet + } + + public void testOrderedLists() { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("Lists.doc"); + Range r = doc.getRange(); + assertEquals(40, r.numParagraphs()); + + assertEquals("Next up is an ordered list:\r", r.getParagraph(5).text()); + assertEquals("Ordered list 1\r", r.getParagraph(6).text()); + assertEquals("OL 2\r", r.getParagraph(7).text()); + assertEquals("OL 3\r", r.getParagraph(8).text()); + assertEquals("Now for an un-ordered list with a different bullet style:\r", r.getParagraph(9).text()); + + assertEquals(9, r.getParagraph(5).getLvl()); + assertEquals(9, r.getParagraph(6).getLvl()); + assertEquals(9, r.getParagraph(7).getLvl()); + assertEquals(9, r.getParagraph(8).getLvl()); + assertEquals(9, r.getParagraph(9).getLvl()); + + assertEquals(0, r.getParagraph(5).getIlvl()); + assertEquals(0, r.getParagraph(6).getIlvl()); + assertEquals(0, r.getParagraph(7).getIlvl()); + assertEquals(0, r.getParagraph(8).getIlvl()); + assertEquals(0, r.getParagraph(9).getIlvl()); + } + + public void testMultiLevelLists() { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("Lists.doc"); + Range r = doc.getRange(); + assertEquals(40, r.numParagraphs()); + + assertEquals("Multi-level un-ordered list:\r", r.getParagraph(12).text()); + assertEquals("ML 1:1\r", r.getParagraph(13).text()); + assertEquals("ML 1:2\r", r.getParagraph(14).text()); + assertEquals("ML 2:1\r", r.getParagraph(15).text()); + assertEquals("ML 2:2\r", r.getParagraph(16).text()); + assertEquals("ML 2:3\r", r.getParagraph(17).text()); + assertEquals("ML 3:1\r", r.getParagraph(18).text()); + assertEquals("ML 4:1\r", r.getParagraph(19).text()); + assertEquals("ML 5:1\r", r.getParagraph(20).text()); + assertEquals("ML 5:2\r", r.getParagraph(21).text()); + assertEquals("ML 2:4\r", r.getParagraph(22).text()); + assertEquals("ML 1:3\r", r.getParagraph(23).text()); + assertEquals("Multi-level ordered list:\r", r.getParagraph(24).text()); + assertEquals("OL 1\r", r.getParagraph(25).text()); + assertEquals("OL 2\r", r.getParagraph(26).text()); + assertEquals("OL 2.1\r", r.getParagraph(27).text()); + assertEquals("OL 2.2\r", r.getParagraph(28).text()); + assertEquals("OL 2.2.1\r", r.getParagraph(29).text()); + assertEquals("OL 2.2.2\r", r.getParagraph(30).text()); + assertEquals("OL 2.2.2.1\r", r.getParagraph(31).text()); + assertEquals("OL 2.2.3\r", r.getParagraph(32).text()); + assertEquals("OL 3\r", r.getParagraph(33).text()); + assertEquals("Finally we want some indents, to tell the difference\r", r.getParagraph(34).text()); + + for(int i=12; i<=34; i++) { + assertEquals(9, r.getParagraph(12).getLvl()); + } + assertEquals(0, r.getParagraph(12).getIlvl()); + assertEquals(0, r.getParagraph(13).getIlvl()); + assertEquals(0, r.getParagraph(14).getIlvl()); + assertEquals(1, r.getParagraph(15).getIlvl()); + assertEquals(1, r.getParagraph(16).getIlvl()); + assertEquals(1, r.getParagraph(17).getIlvl()); + assertEquals(2, r.getParagraph(18).getIlvl()); + assertEquals(3, r.getParagraph(19).getIlvl()); + assertEquals(4, r.getParagraph(20).getIlvl()); + assertEquals(4, r.getParagraph(21).getIlvl()); + assertEquals(1, r.getParagraph(22).getIlvl()); + assertEquals(0, r.getParagraph(23).getIlvl()); + assertEquals(0, r.getParagraph(24).getIlvl()); + assertEquals(0, r.getParagraph(25).getIlvl()); + assertEquals(0, r.getParagraph(26).getIlvl()); + assertEquals(1, r.getParagraph(27).getIlvl()); + assertEquals(1, r.getParagraph(28).getIlvl()); + assertEquals(2, r.getParagraph(29).getIlvl()); + assertEquals(2, r.getParagraph(30).getIlvl()); + assertEquals(3, r.getParagraph(31).getIlvl()); + assertEquals(2, r.getParagraph(32).getIlvl()); + assertEquals(0, r.getParagraph(33).getIlvl()); + assertEquals(0, r.getParagraph(34).getIlvl()); + } + + public void testIndentedText() { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("Lists.doc"); + Range r = doc.getRange(); + + assertEquals(40, r.numParagraphs()); + assertEquals("Finally we want some indents, to tell the difference\r", r.getParagraph(34).text()); + assertEquals("Indented once\r", r.getParagraph(35).text()); + assertEquals("Indented twice\r", r.getParagraph(36).text()); + assertEquals("Indented three times\r", r.getParagraph(37).text()); + assertEquals("The end!\r", r.getParagraph(38).text()); + + assertEquals(9, r.getParagraph(34).getLvl()); + assertEquals(9, r.getParagraph(35).getLvl()); + assertEquals(9, r.getParagraph(36).getLvl()); + assertEquals(9, r.getParagraph(37).getLvl()); + assertEquals(9, r.getParagraph(38).getLvl()); + assertEquals(9, r.getParagraph(39).getLvl()); + + assertEquals(0, r.getParagraph(34).getIlvl()); + assertEquals(0, r.getParagraph(35).getIlvl()); + assertEquals(0, r.getParagraph(36).getIlvl()); + assertEquals(0, r.getParagraph(37).getIlvl()); + assertEquals(0, r.getParagraph(38).getIlvl()); + assertEquals(0, r.getParagraph(39).getIlvl()); + + // TODO Test the indent + } + + public void testWriteRead() throws IOException { + HWPFDocument doc = HWPFTestDataSamples.openSampleFile("Lists.doc"); + doc = HWPFTestDataSamples.writeOutAndReadBack(doc); + + Range r = doc.getRange(); + + // Check a couple at random + assertEquals(4, r.getParagraph(21).getIlvl()); + assertEquals(1, r.getParagraph(22).getIlvl()); + assertEquals(0, r.getParagraph(23).getIlvl()); + } +} diff --git a/test-data/document/Lists.doc b/test-data/document/Lists.doc new file mode 100644 index 0000000000000000000000000000000000000000..80201eef14104bc2383a8bdd51dab7452d9319d1 GIT binary patch literal 27648 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