blob: e00b9448f52e39b88cda0c3695c481974dac3903 [file] [log] [blame]
/*
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.odftoolkit.simple.text.list;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.odftoolkit.odfdom.dom.OdfContentDom;
import org.odftoolkit.odfdom.dom.element.table.TableTableCellElementBase;
import org.odftoolkit.odfdom.dom.element.text.TextListElement;
import org.odftoolkit.odfdom.dom.element.text.TextListHeaderElement;
import org.odftoolkit.odfdom.dom.element.text.TextListItemElement;
import org.odftoolkit.odfdom.dom.element.text.TextListLevelStyleBulletElement;
import org.odftoolkit.odfdom.dom.element.text.TextListLevelStyleElementBase;
import org.odftoolkit.odfdom.dom.element.text.TextListLevelStyleImageElement;
import org.odftoolkit.odfdom.dom.element.text.TextListLevelStyleNumberElement;
import org.odftoolkit.odfdom.dom.element.text.TextPElement;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeAutomaticStyles;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
import org.odftoolkit.odfdom.incubator.doc.text.OdfTextListStyle;
import org.odftoolkit.odfdom.pkg.OdfElement;
import org.odftoolkit.odfdom.pkg.OdfFileDom;
import org.odftoolkit.simple.Document;
import org.odftoolkit.simple.text.list.ListDecorator.ListType;
import org.w3c.dom.Node;
/**
* This class represents a list. It can contain list header,
* followed by list items.
*
* @since 0.4
*/
public class List {
private TextListElement listElement;
private ListDecorator decorator;
/**
* Constructor ListItem, AbstractListContainer and List use only.
*
* @param element
* the ODF element
*/
List(TextListElement element) {
listElement = element;
// only getListType and addItem --> item.setParagraphDecorator() need
// decorator. We have fixed the problems when decorator is null in these
// two conditions. So decorator null is OK for a exist list. But this
// constructor should not be used on a new created TextListElement, as
// its style name may have not set.
decorator = null;
}
/**
* Constructor with ListContainer only. A bullet list with default style
* will be created.
*
* @param container
* the container in where this list will be appended.
*/
public List(ListContainer container) {
this(container, null, null);
}
/**
* Constructor with ListContainer and ListDecorator.
*
* @param container
* the container in where this list will be appended.
* @param decorator
* the ListDecorator of this list.
*/
public List(ListContainer container, ListDecorator decorator) {
this(container, null, decorator);
}
/**
* Constructor with ListContainer, ListDecorator and header.
*
* @param container
* the container in where this list will be appended.
* @param decorator
* the ListDecorator of this list.
* @param header
* the header of this list.
*/
public List(ListContainer container, String header, ListDecorator decorator) {
OdfElement containerElement = container.getListContainerElement();
OdfFileDom ownerDocument = (OdfFileDom) containerElement.getOwnerDocument();
listElement = ownerDocument.newOdfElement(TextListElement.class);
listElement.setXmlIdAttribute(getUniqueXMLID());
containerElement.appendChild(listElement);
setHeader(header);
if (decorator == null) {
Document doc = (Document) ownerDocument.getDocument();
decorator = new BulletDecorator(doc);
}
this.decorator = decorator;
decorator.decorateList(this);
}
/**
* Constructor with ListContainer, ListDecorator, header and numbering setting.
*
* @param container
* the container in where this list will be appended.
* @param decorator
* the ListDecorator of this list.
* @param isContinueNumbering
* If <code>isContinueNumbering</code> is true, the numbering of this list is
* continuing, otherwise the numbering of this list starts from the beginning.
* @param header
* the header of this list.
*/
public List(ListContainer container, String header, boolean isContinueNumbering, ListDecorator decorator) {
this(container, header, decorator);
setContinueNumbering(isContinueNumbering);
}
/**
* Constructor with ListContainer, ListDecorator, header and continued list
*
* @param container
* the container in where this list will be appended.
* @param decorator
* the ListDecorator of this list.
* @param continueList
* the continued list of this list.
* @param header
* the header of this list.
*/
public List(ListContainer container, String header, List continueList, ListDecorator decorator) {
this(container, header, decorator);
setContinueList(continueList);
}
/**
* Get the type of this list. The list type can be BULLET, NUMBER and IMAGE.
*
* @return the list type.
*/
public ListType getType() {
if (decorator != null) {
return decorator.getListType();
} else {
try {
String textStyleName = listElement.getTextStyleNameAttribute();
Document doc = (Document) (((OdfFileDom) listElement.getOwnerDocument()).getDocument());
OdfContentDom contentDocument = doc.getContentDom();
OdfOfficeAutomaticStyles styles = contentDocument.getAutomaticStyles();
OdfOfficeStyles documentStyles = doc.getDocumentStyles();
OdfTextListStyle listStyle = styles.getListStyle(textStyleName);
if (listStyle == null) {
listStyle = documentStyles.getListStyle(textStyleName);
}
if (listStyle != null) {
TextListLevelStyleElementBase listLevelStyle = listStyle.getLevel(1);
if (listLevelStyle instanceof TextListLevelStyleBulletElement) {
return ListType.BULLET;
} else if (listLevelStyle instanceof TextListLevelStyleNumberElement) {
return ListType.NUMBER;
} else if (listLevelStyle instanceof TextListLevelStyleImageElement) {
return ListType.IMAGE;
}
}
} catch (Exception e) {
Logger.getLogger(List.class.getName()).log(Level.SEVERE, null, e);
}
return null;
}
}
/**
* Get the header of this list.
*
* @return the header of this list.
*/
public String getHeader() {
String header = "";
Node headerNode = listElement.getFirstChild();
if (headerNode instanceof TextListHeaderElement) {
Node pNode = headerNode.getFirstChild();
String splitString = "";
while (pNode != null) {
if (pNode instanceof TextPElement) {
String content = pNode.getTextContent();
if ((content != null) && (content.length() > 0)) {
header = header + splitString + content;
splitString = "\n";
}
}
pNode = pNode.getNextSibling();
}
}
if ("".equals(header)) {
return null;
} else {
return header;
}
}
/**
* Set the header of this list. The exist header will be replaced.
*
* @param header
* the header to be set.
*/
public void setHeader(String header) {
if (header != null) {
String[] headerContents = header.split("\n");
TextListHeaderElement listHeaderElement = null;
Node firstNode = listElement.getFirstChild();
if (firstNode instanceof TextListHeaderElement) {
listHeaderElement = (TextListHeaderElement) firstNode;
Node pElement = listHeaderElement.getFirstChild();
while (pElement != null) {
firstNode.removeChild(pElement);
pElement = pElement.getNextSibling();
}
} else {
listHeaderElement = ((OdfFileDom) listElement.getOwnerDocument())
.newOdfElement(TextListHeaderElement.class);
listElement.insertBefore(listHeaderElement, firstNode);
}
for (String headerContent : headerContents) {
TextPElement pElement = listHeaderElement.newTextPElement();
pElement.setTextContent(headerContent);
}
}
}
/**
* Set the ListDecorator of this list. The current ListDecorator will be
* replaced.
* <p>
* This is a useful method which can change the list type and style, even
* though it has been created.
*
* @param decorator
* the ListDecorator to be used.
*/
public void setDecorator(ListDecorator decorator) {
if (decorator != null) {
this.decorator = decorator;
decorator.decorateList(this);
}
}
/**
* Add a list item by specifying a string value.
*
* @param itemContent
* the list item content to be added.
* @return the added ListItem.
*/
public ListItem addItem(String itemContent) {
TextListItemElement listItemElement = listElement.newTextListItemElement();
ListItem item = new ListItem(listItemElement);
item.setParagraphDecorator(decorator);
item.setTextContent(itemContent);
return item;
}
/**
* Insert the specified ListItem at the specified location.
* The ListItem is inserted before the ListItem at the specified
* location.
*
* @param location
* the index to insert. The start number is 0.
* @param itemContent
* the list item content to be added.
* @return the added ListItem.
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public ListItem addItem(int location, String itemContent) {
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
TextListItemElement listItemElement = ownerDocument.newOdfElement(TextListItemElement.class);
Node refNode = getItemByLocation(location);
listElement.insertBefore(listItemElement, refNode);
ListItem item = new ListItem(listItemElement);
item.setParagraphDecorator(decorator);
item.setTextContent(itemContent);
return item;
}
/**
* Add the specified list item in ListItem object.
*
* @param item
* the list item to be added.
* @return the added ListItem.
*/
public ListItem addItem(ListItem item) {
TextListItemElement itemElement = (TextListItemElement) (item.getOdfElement().cloneNode(true));
listElement.appendChild(itemElement);
ListItem newItem = new ListItem(itemElement);
return newItem;
}
/**
* Insert a ListItem at the specified location.
* The ListItem is inserted before the ListItem at the specified
* location.
*
* @param location
* the index to insert.
* @param item
* the list item to add.
* @return the added ListItem.
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public ListItem addItem(int location, ListItem item) {
TextListItemElement itemElement = (TextListItemElement) (item.getOdfElement().cloneNode(true));
Node refNode = getItemByLocation(location);
listElement.insertBefore(itemElement, refNode);
ListItem newItem = new ListItem(itemElement);
return newItem;
}
/**
* Add list items by specifying an array of string values.
*
* @param items
* the list items to be added.
* @return the added items.
*/
public java.util.List<ListItem> addItems(String[] items) {
java.util.List<ListItem> itemList = new java.util.ArrayList<ListItem>();
for (String itemString : items) {
TextListItemElement listItemElement = listElement.newTextListItemElement();
ListItem item = new ListItem(listItemElement);
item.setTextContent(itemString);
itemList.add(item);
}
return itemList;
}
/**
* Insert the list items at the specified location in this List
* by giving an array of string values.
*
* @param location
* the index to insert.
* @param items
* the collection of list item contents.
* @return the added items.
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public java.util.List<ListItem> addItems(int location, String[] items) {
java.util.List<ListItem> listCollection = new ArrayList<ListItem>();
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
Node refNode = getItemByLocation(location);
for (int i = items.length - 1; i >= 0; i--) {
TextListItemElement listItemElement = ownerDocument.newOdfElement(TextListItemElement.class);
listElement.insertBefore(listItemElement, refNode);
ListItem item = new ListItem(listItemElement);
item.setParagraphDecorator(decorator);
item.setTextContent(items[i]);
refNode = listItemElement;
listCollection.add(item);
}
return listCollection;
}
/**
* Add list items by specifying an array of ListItem.
*
* @param items
* the list items to be added.
*/
public java.util.List<ListItem> addItems(ListItem[] items) {
java.util.List<ListItem> itemList = new java.util.ArrayList<ListItem>();
for (ListItem itemClone : items) {
TextListItemElement itemElement = (TextListItemElement) (itemClone.getOdfElement().cloneNode(true));
listElement.appendChild(itemElement);
itemList.add(new ListItem(itemElement));
}
return itemList;
}
/**
* Insert the list items at the certain location
* by specifying an array of ListItem.
*
* @param location
* the index to insert.
* @param items
* the collection of items.
* @return the added items
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public java.util.List<ListItem> addItems(int location, ListItem[] items) {
java.util.List<ListItem> listCollection = new ArrayList<ListItem>();
Node refNode = getItemByLocation(location);
for (int i = items.length - 1; i >= 0; i--) {
TextListItemElement itemElement = (TextListItemElement) (items[i].getOdfElement().cloneNode(true));
listElement.insertBefore(itemElement, refNode);
ListItem item = new ListItem(itemElement);
refNode = itemElement;
listCollection.add(item);
}
return listCollection;
}
/**
* Return the item at the specified location in this List.
*
* @param location
* the index of the element to be returned.
* @return the element at the specified location.
*
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public ListItem getItem(int location) {
return new ListItem(getItemByLocation(location));
}
/**
* Get all of the list items.
*
* @return all of list items.
*/
public java.util.List<ListItem> getItems() {
java.util.List<ListItem> itemList = new java.util.ArrayList<ListItem>();
Node firstNode = listElement.getFirstChild();
while (firstNode != null) {
if (firstNode instanceof TextListItemElement) {
itemList.add(new ListItem((TextListItemElement) firstNode));
}
firstNode = firstNode.getNextSibling();
}
return itemList;
}
/**
* Return the number of direct child items in this List.
*
* @return the number of direct child items in this List.
*/
public int size() {
int size = 0;
Node firstNode = listElement.getFirstChild();
while (firstNode != null) {
if (firstNode instanceof TextListItemElement) {
size++;
}
firstNode = firstNode.getNextSibling();
}
return size;
}
/**
* Replace the item at the specified location in this List with the
* specified item.
*
* @param location
* the index to put the specified item.
* @param item
* the new item to be added.
* @return the previous element at the index.
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public ListItem set(int location, ListItem item) {
TextListItemElement itemElement = (TextListItemElement) (item.getOdfElement().cloneNode(true));
Node oldNode = getItemByLocation(location);
listElement.replaceChild(itemElement, oldNode);
ListItem newItem = new ListItem(itemElement);
newItem.setParagraphDecorator(decorator);
return newItem;
}
/**
* Replace the item at the specified location in this List with the
* specified item content.
*
* @param location
* the index to insert. The start number is 0.
* @param itemContent
* the list item content to be added.
* @return the previous element at the index.
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public ListItem set(int location, String itemContent) {
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
TextListItemElement listItemElement = ownerDocument.newOdfElement(TextListItemElement.class);
Node oldNode = getItemByLocation(location);
listElement.replaceChild(listItemElement, oldNode);
ListItem item = new ListItem(listItemElement);
item.setParagraphDecorator(decorator);
item.setTextContent(itemContent);
return item;
}
/**
* Remove the item at the specified location from this List.
*
* @param location
* the index of the item to be removed.
* @return true if this List is modified, false otherwise.
* @exception IndexOutOfBoundsException
* when the <code>location</code> is out of the List range.
*/
public boolean removeItem(int location) {
TextListItemElement itemElement = getItemByLocation(location);
if (itemElement == null) {
return false;
} else {
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
Document doc = (Document) ownerDocument.getDocument();
doc.removeElementLinkedResource(itemElement);
listElement.removeChild(itemElement);
return true;
}
}
/**
* Remove the specified item from this List.
*
* @param item
* the item to be removed.
* @return true if this List is modified, false otherwise.
*/
public boolean removeItem(ListItem item) {
TextListItemElement itemElement = item.getOdfElement();
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
Document doc = (Document) ownerDocument.getDocument();
doc.removeElementLinkedResource(itemElement);
Node removedNode = listElement.removeChild(itemElement);
if (removedNode == null) {
return false;
} else {
return true;
}
}
/**
* Remove all the items in the collection from this list
*
* @param items
* the collection of items to be removed.
* @return true if this List is modified, false otherwise.
*/
public boolean removeItems(java.util.List<ListItem> items) {
boolean listChanged = false;
for (ListItem item : items) {
TextListItemElement itemElement = item.getOdfElement();
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
Document doc = (Document) ownerDocument.getDocument();
doc.removeElementLinkedResource(itemElement);
Node removedNode = listElement.removeChild(itemElement);
if (removedNode != null) {
listChanged = true;
}
}
return listChanged;
}
/**
* Remove all items from this List.
*/
public void clear() {
Node firstChild = listElement.getFirstChild();
while (firstChild != null) {
if (firstChild instanceof TextListItemElement) {
Node removedNode = firstChild;
firstChild = firstChild.getNextSibling();
listElement.removeChild(removedNode);
} else {
firstChild = firstChild.getNextSibling();
}
}
}
/**
* Remove this list from its container.
*/
public void remove() {
Node parentElement = listElement.getParentNode();
OdfFileDom ownerDocument = (OdfFileDom) listElement.getOwnerDocument();
Document doc = (Document) ownerDocument.getDocument();
doc.removeElementLinkedResource(listElement);
parentElement.removeChild(listElement);
}
/**
* Return whether the numbering of this list is continuing,
* or whether the numbering of the preceding list is continued or not.
*
* @return true if the numbering of this list is continuing,
* false if not.
*/
public boolean isContinueNumbering() {
Boolean isContinueNumbering = listElement.getTextContinueNumberingAttribute();
if (isContinueNumbering == null) {
String continueList = listElement.getTextContinueListAttribute();
if (continueList != null) {
return true;
} else {
return false;
}
} else {
return isContinueNumbering;
}
}
/**
* Set whether the numbering of the preceding list is continued or not. This
* method will set the attribute "text:continue-numbering" of list element.
* As ODF specification describes, this attribute is ignored, if attribute
* <text:continue-list> is present, the user can call
* <code>setContinueList(List)</code> to set this attribute. This method is
* a easy way to set a list continue numbering, while
* <code>setContinueList(List)</code> is an advance way to set a list
* numbering. For example, there are three lists ListA, ListB and ListC in
* order. If the user call set ListC.setContinueNumbering, the first list
* item in ListC is the number of the last item in ListB incremented by one.
* It easy, no need to get the reference of ListB. While if the user need
* the first list item in ListC is the number of the last item in ListA
* incremented by one, he must use ListC.setContinueList(ListA).
*
*
* @param isContinueNumbering
* If <code>isContinueNumbering</code> is true, and
* text:continue-list attribute is not present and the numbering
* style of the preceding list is the same as the current list,
* the number of the first list item in the current list is the
* number of the last item in the preceding list incremented by
* one.the list is continue numbering, otherwise if the
* text:continue-list attribute is not present, the numbering of
* the preceding list is not continued.
* @see #setContinueList(List)
*/
public void setContinueNumbering(boolean isContinueNumbering) {
if (getType() == ListType.NUMBER) {
listElement.setTextContinueNumberingAttribute(isContinueNumbering);
}
}
/**
* Get the preceding list whose numbering is continued by this list.
* <p>Now only support to get the continued list reference in the same ListContainer and the same Level.
*
* @return the continued list of this list. If the list has no continued list,
* it will return null.
*/
public List getContinueList() {
List continueList = null;
if (isContinueNumbering()) {
TextListElement continueListElement = null;
String continueListID = listElement.getTextContinueListAttribute();
if (continueListID != null) {
Node parentElement = listElement.getParentNode();
Node firstNode = parentElement.getFirstChild();
while (firstNode != null) {
if (firstNode instanceof TextListElement) {
TextListElement listElement = (TextListElement) firstNode;
String xmlID = listElement.getXmlIdAttribute();
if (continueListID.equals(xmlID)) {
continueListElement = (TextListElement) firstNode;
break;
}
}
firstNode = firstNode.getNextSibling();
}
} else {
Node preNode = listElement.getPreviousSibling();
while (preNode != null) {
if (preNode instanceof TextListElement) {
continueListElement = (TextListElement) preNode;
break;
}
preNode = preNode.getPreviousSibling();
}
}
continueList = new List(continueListElement);
}
return continueList;
}
/**
* Set the list whose numbering is continued by this list.
* This is an advance way to set a list
* numbering. For example, there are three lists ListA, ListB and ListC in
* order. If the user needs the first list item in ListC is the number of the
* last item in ListA incremented by one, he must use
* ListC.setContinueList(ListA).
*
* @param continueList
* the continued list of this list.
* @see #setContinueNumbering(boolean)
*/
public void setContinueList(List continueList) {
if (getType() == ListType.NUMBER) {
String xmlId = continueList.listElement.getXmlIdAttribute();
if (xmlId != null) {
listElement.setTextContinueListAttribute(xmlId);
} else {
xmlId = getUniqueXMLID();
continueList.listElement.setXmlIdAttribute(xmlId);
listElement.setTextContinueListAttribute(xmlId);
}
listElement.setTextContinueNumberingAttribute(true);
}
}
/**
* Get the level of this list.
* <p>
* Every list has a list level. If a list is not contained in another list,
* its list level is 1. If a list is contained within another list, the list
* level of the contained list is the list level of the list in which it is
* contained incremented by one. If a list is contained in a table cell or
* text box, its list level returns to 1, even though the table or text box
* may be nested in another list. Every list with a list level of 1 defines
* a list and the counter domain for its list items and any sub list of that
* list. Each sub list starts a counter for its list items and any sub list
* it may contain.
*
* @return list level.
*/
public int getLevel() {
int level = 1;
Node parentNode = listElement.getParentNode();
while (parentNode != null) {
if (parentNode instanceof TextListElement) {
level++;
}
if (parentNode instanceof TableTableCellElementBase) {
break;
}
parentNode = parentNode.getParentNode();
}
return level;
}
/**
* Get the instance of TextListElement which represents this list.
*
* @return the instance of TextListElement.
*/
public TextListElement getOdfElement() {
return listElement;
}
@Override
public String toString() {
StringBuilder strBuilder = new StringBuilder();
int level = getLevel();
String levelPrefix = "";
int i = 1;
while (i < level) {
levelPrefix += " ";
i++;
}
String splitStr = "";
String header = getHeader();
if (header != null) {
strBuilder.append(levelPrefix);
strBuilder.append(header);
splitStr = "\n";
}
String itemPrefix = "• ";
int j = 0;
if (getType() == ListType.NUMBER) {
itemPrefix = ". ";
j = 1;
}
for (ListItem item : getItems()) {
strBuilder.append(splitStr);
strBuilder.append(levelPrefix);
if (j > 0) {
strBuilder.append(j++);
}
strBuilder.append(itemPrefix);
strBuilder.append(item.toString());
splitStr = "\n";
}
return strBuilder.toString();
}
// find the insert node.
private TextListItemElement getItemByLocation(int location) {
if (location < 0) {
throw new IndexOutOfBoundsException("the location " + location + " is is out of the List range.");
}
Node firstNode = listElement.getFirstChild();
TextListItemElement positionNode = null;
int i = 0;
while (firstNode != null) {
if (firstNode instanceof TextListItemElement) {
if (i == location) {
break;
}
i++;
}
firstNode = firstNode.getNextSibling();
}
if ((i == location) && (firstNode instanceof TextListItemElement)) {
positionNode = (TextListItemElement) firstNode;
}
if ((location != 0) && (i < location)) {
throw new IndexOutOfBoundsException("the location " + location + " is is out of the List range.");
}
return positionNode;
}
private String getUniqueXMLID() {
return "list" + Math.round(Math.random() * 100000000);
}
/**
* Answers an Iterator on the items of this List. The items are iterated in
* the same order that they occur in the List.
*
* @return an Iterator on the items of this List
*
* @see java.util.Iterator
*/
/*
* public Iterator<ListItem> iterator() { throw new
* UnsupportedOperationException("todo"); }
*/
}