/*
* Copyright 2013, Plutext Pty Ltd.
*
* This file is part of the Commercial Edition of docx4j,
* which is licensed under the Plutext Commercial License (the "License");
* you may not use this file except in compliance with the License.
*
* In particular, this source code is CONFIDENTIAL, and you must ensure it
* stays that way.
*
* You may obtain a copy of the License at
*
* http://www.plutext.com/license/Plutext_Commercial_License.pdf
*
* 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.docx4j.toc.switches;
import java.io.StringWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import org.docx4j.TextUtils;
import org.docx4j.TraversalUtil;
import org.docx4j.XmlUtils;
import org.docx4j.finders.RangeFinder;
import org.docx4j.jaxb.Context;
import org.docx4j.model.PropertyResolver;
import org.docx4j.model.listnumbering.Emulator.ResultTriple;
import org.docx4j.model.structure.PageDimensions;
import org.docx4j.openpackaging.exceptions.InvalidFormatException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.toc.StyleBasedOnHelper;
import org.docx4j.toc.TocEntry;
import org.docx4j.wml.CTBookmark;
import org.docx4j.wml.CTMarkupRange;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;
import org.docx4j.wml.STTabTlc;
import org.docx4j.wml.Style;
import org.docx4j.wml.Text;
import org.docx4j.wml.PPrBase.PStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SwitchProcessor {
private static Logger log = LoggerFactory.getLogger(SwitchProcessor.class);
protected PropertyResolver propertyResolver;
protected StyleBasedOnHelper styleBasedOnHelper;
private PageDimensions pageDimensions;
private static final String TOC_PREFIX = "_Toc";
private static final int MILLION = 1000000;
private TocEntry entry = null;
private STTabTlc leader;
private boolean proceed = true;
private boolean styleFound = false;
private boolean pageNumbers = true;
// public SwitchProcessor(PageDimensions pageDimensions) {
//
// this.pageDimensions = pageDimensions;
// }
public SwitchProcessor(PageDimensions pageDimensions, STTabTlc leader) {
this.pageDimensions = pageDimensions;
this.leader = leader;
}
public List<TocEntry> processSwitches(WordprocessingMLPackage wordMLPackage, List<P> pList,
List<SwitchInterface> switchesList,
Map<P, ResultTriple> pNumbersMap){
List<TocEntry> tocEntries = new ArrayList<TocEntry>();
PPr ppr;
PStyle pStyle;
Style s;
propertyResolver = wordMLPackage.getMainDocumentPart().getPropertyResolver();
styleBasedOnHelper = new StyleBasedOnHelper(propertyResolver);
int randomSeed = (int)(Math.random()*MILLION);
AtomicInteger seedIndex = getBookmarkId(wordMLPackage);
String anchorValue = "";
for(P p: pList){
ppr = p.getPPr();
if(ppr == null){ // TODO FIXME: Normal could be required in TOC
continue;
}
pStyle = ppr.getPStyle();
if(pStyle == null){
// TODO FIXME an outline level may be enough for this p to appear in the TOC?
log.debug("P has no style; ignoring..");
continue;
}
/*
* Could redo this.
*
* Keep a cache of known TOC'd styles.
* (If we don't, consider adding caches in T and O switches)
*
* Only U can cause exclusion, so handle this first:
* If one of the switches is U, check pPr for outline level,
* and set !proceed if appropriate.
*
* Otherwise, check the cache for this style.
* If not cached, work it out, and cache the result.
*
* Some switches are for formatting eg \h, \n
* Others are for inclusion/exclusion eg \\u, \o, \t
*
* Consider creating subclasses, and processing separately.
*
*/
s = propertyResolver.getStyle(pStyle.getVal());
log.debug("Processing switches for stylename " + pStyle.getVal() );
if (s==null) {
log.warn("Style " + pStyle.getVal() + " is not defined in styles part!");
continue;
/*
* TODO It would be possible to do some switch processing even though
* the style is not defined, since we could base some processing
* on the stylename. However doing this would require passing
* stylename into the process method so we could fall back to it
* when s==null
*/
}
int i = 1;
OSwitch oSwitch = null;
for(SwitchInterface sw: switchesList){
if (sw instanceof OSwitch) {
oSwitch = (OSwitch)sw;
}
}
for(SwitchInterface sw: switchesList){
log.debug(sw.getClass().getName());
if (sw instanceof USwitch) {
// Need pPr
((USwitch)sw).process(s, this, ppr, oSwitch);
} else {
sw.process(s, this);
}
if(!proceed){
log.debug(sw.getClass().getName() + " forced break!");
// the \\u switch (outline level) will do this if the outline level is 9
break;
}
// processed all style switches and style was not found
if(!sw.isStyleSwitch()){
if(!styleFound){
log.debug("processed all style switches and style was not found");
proceed = false;
break;
}
}
// last style switch processed and style not found
else {
if(i == switchesList.size()){
if(!styleFound){
log.debug("last style switch processed and style not found");
proceed = false;
break;
}
}
}
i++;
}
if(proceed){
{
// create the visible entry text
entry.setEntryValue(p);
// number the paragraph, if necessary
entry.numberEntry(pNumbersMap.get(p));
}
// If the p is empty, omit it UNLESS it is numbered,
// in which case Word includes it.
if (entry.getEntryValue()==null
|| entry.getEntryValue().size()==0) {
log.debug("Not adding p to toc since it is empty. (stylename " + pStyle.getVal() );
} else {
log.debug("Adding p to toc with stylename " + pStyle.getVal() );
anchorValue = TOC_PREFIX + randomSeed + seedIndex;
bookmarkStyleText(p, anchorValue, seedIndex, entry);
entry.setAnchorValue(anchorValue);
tocEntries.add(entry);
//seedIndex++;
}
}
//set default for new iteration
entry = null;
proceed = true;
styleFound = false;
}
return tocEntries;
}
private void bookmarkStyleText(P p, String anchorValue, AtomicInteger id, TocEntry te){
CTBookmark bookmark = null;
// R r = null;
// Text t;
for(Object o : p.getContent()){
if(o instanceof CTBookmark){
bookmark = (CTBookmark)o;
break;
}
}
// else if(o instanceof R){
// r = (R)o;
// Object o2= XmlUtils.unwrap(r.getContent().get(0));
// if (o2 instanceof Text) {
// t = (Text)o2;
//// if(t == null){
//// return false;
//// }
// entryValue = t.getValue();
// } else {
// log.warn("Unexpected entry in run " + o2.getClass().getName());
// return false;
// }
// }
// }
//
// if(r == null){
// return false;
// }
if(bookmark == null){
bookmarkP(p, anchorValue, id.getAndIncrement());
} else {
bookmark.setId(BigInteger.valueOf(id.getAndIncrement()));
bookmark.setName(anchorValue);
}
}
/**
* Surround the specified r in the specified p
* with a bookmark (with specified name and id)
* @param p
* @param r
* @param name
* @param id
*/
public void bookmarkP(P p, String name, int id) {
// Enhancement: If the P was already bookmarked,
// it would be nice to be able to just re-use
// the existing bookmark here.
// // Find the index
// int index = p.getContent().indexOf(r);
//
// if (index<0) {
// System.out.println("P does not contain R!");
// return;
// }
ObjectFactory factory = Context.getWmlObjectFactory();
BigInteger ID = BigInteger.valueOf(id);
// Add bookmark end first
CTMarkupRange mr = factory.createCTMarkupRange();
mr.setId(ID);
JAXBElement<CTMarkupRange> bmEnd = factory.createBodyBookmarkEnd(mr);
p.getContent().add(bmEnd);
// Next, bookmark start
CTBookmark bm = factory.createCTBookmark();
bm.setId(ID);
bm.setName(name);
JAXBElement<CTBookmark> bmStart = factory.createBodyBookmarkStart(bm);
p.getContent().add(0, bmStart);
}
public void setPageNumbers(boolean pageNumbers){
this.pageNumbers = pageNumbers;
}
public boolean pageNumbers(){
return pageNumbers;
}
public TocEntry getEntry(){
if(entry == null){
entry = new TocEntry(propertyResolver, pageDimensions, leader);
}
return entry;
}
public void proceed(boolean proceed){
this.proceed = proceed;
}
public boolean isStyleFound() {
return styleFound;
}
public void setStyleFound(boolean headingFound) {
this.styleFound = headingFound;
}
private AtomicInteger bookmarkId = null;
/**
* Provide a way to set the starting bookmark ID number.
*
* For efficiency, user code needs to pass this value through.
*
* If it isn't, the value will be calculated (less efficient).
*
* @param bookmarkId
* @since 3.3.0
*/
public void setStartingIdForNewBookmarks(AtomicInteger bookmarkId) {
this.bookmarkId = bookmarkId;
}
protected AtomicInteger getBookmarkId(WordprocessingMLPackage wordMLPackage) {
if (bookmarkId==null) {
// Work out starting ID
bookmarkId = new AtomicInteger(initBookmarkIdStart(wordMLPackage));
}
return bookmarkId;
}
private int initBookmarkIdStart(WordprocessingMLPackage wordMLPackage) {
int highestId = 0;
RangeFinder rt = new RangeFinder("CTBookmark", "CTMarkupRange");
new TraversalUtil(wordMLPackage.getMainDocumentPart().getContent(), rt);
for (CTBookmark bm : rt.getStarts()) {
BigInteger id = bm.getId();
if (id!=null && id.intValue()>highestId) {
highestId = id.intValue();
}
}
return highestId +1;
}
}