package org.docx4j.model.datastorage;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.xml.bind.JAXBElement;
import org.docx4j.TraversalUtil;
import org.docx4j.TraversalUtil.CallbackImpl;
import org.docx4j.XmlUtils;
import org.docx4j.finders.RangeFinder;
import org.docx4j.jaxb.Context;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.CTBookmark;
import org.docx4j.wml.CTMarkupRange;
import org.docx4j.wml.CTPerm;
import org.docx4j.wml.ContentAccessor;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.docx4j.wml.RangePermissionStart;
import org.docx4j.wml.Text;
import org.jvnet.jaxb2_commons.ppp.Child;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BookmarkRenumber {
/*
* Note that where the input docx contains:
*
<w:bookmarkStart w:id="309" w:name="_Ref353188010" w:displacedByCustomXml="next"/>
<w:sdt>
<w:sdtPr>
<w:alias w:val="Data value: lF2HS"/>
<w:tag w:val="od:repeat=lF2HS"/>
<w:id w:val="-414473234"/>
</w:sdtPr>
<w:sdtContent>
<w:bookmarkEnd w:id="309" w:displacedByCustomXml="prev"/>
A new start element will be created inside the sdtContent.
So later, we may need to sweep through and remove the original bookmarkStart w:id="309",
since it won't have a matching end tag anymore.
*/
protected static Logger log = LoggerFactory.getLogger(BookmarkRenumber.class);
private BookmarkRenumber() {}
BookmarkRenumber(WordprocessingMLPackage wordMLPackage) {
this.wordMLPackage=wordMLPackage;
}
private WordprocessingMLPackage wordMLPackage; // so we can calculate bookmark starting ID on demand
private AtomicInteger bookmarkId = null;
protected AtomicInteger getBookmarkId() {
if (bookmarkId==null) {
// Work out starting ID
bookmarkId = new AtomicInteger(initBookmarkIdStart());
}
return bookmarkId;
}
private int initBookmarkIdStart() {
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;
}
// fixRange( blockRange, "CTBookmark", "CTMarkupRange", null);
protected void fixRange(List<Object> paragraphs, String startElement,
String endElement, String refElement, long global, int instanceNumber) throws Exception {
RangeTraverser rt = new RangeTraverser(startElement, endElement,
refElement);
new TraversalUtil(paragraphs, rt);
Method startIdMethod = null;
Method endIdMethod = null;
// Delete unwanted _GoBack bookmark
if (startElement.equals("CTBookmark")) {
for (CTBookmark bm : rt.deletes) {
BigInteger unwantedId = bm.getId();
try {
// Can't just remove the object from the parent,
// since in the parent, it may be wrapped in a JAXBElement
List<Object> theList = null;
if (bm.getParent() instanceof List) {
theList = (List)bm.getParent(); // eg blockRange.getContents()
} else {
theList = ((ContentAccessor)(bm.getParent())).getContent();
}
Object deleteMe = null;
for (Object ox : theList) {
if (XmlUtils.unwrap(ox).equals(bm)) {
deleteMe = ox;
break;
}
}
if (deleteMe!=null) {
theList.remove(deleteMe);
}
} catch (ClassCastException e) {
log.error(e.getMessage(), e);
}
// Now delete the closing tag
// .. find it
for (Object o : rt.ends) {
if (endIdMethod == null)
endIdMethod = findGetIdMethod(o);
Object id = null;
try {
// BigInteger
id = getId(endIdMethod, o);
if (unwantedId.compareTo((BigInteger)id)==0) {
// Found it
try {
CTMarkupRange mr = (CTMarkupRange)o;
List<Object> theList = null;
if (mr.getParent() instanceof List) {
theList = (List)mr.getParent(); // eg blockRange.getContents()
} else {
theList = ((ContentAccessor)(mr.getParent())).getContent();
}
Object deleteMe = null;
for (Object ox : theList) {
if (XmlUtils.unwrap(ox).equals(mr)) {
deleteMe = ox;
break;
}
}
if (deleteMe!=null) {
theList.remove(deleteMe);
}
} catch (ClassCastException e) {
log.error(e.getMessage(), e);
}
rt.ends.remove(o);
break;
}
} catch (ClassCastException cce) {
// String
id = getIdString(endIdMethod, o);
if (unwantedId.toString().equals(id) ) {
// Found it
try {
CTMarkupRange mr = (CTMarkupRange)o;
List<Object> theList = null;
if (mr.getParent() instanceof List) {
theList = (List)mr.getParent(); // eg blockRange.getContents()
} else {
theList = ((ContentAccessor)(mr.getParent())).getContent();
}
Object deleteMe = null;
for (Object ox : theList) {
if (XmlUtils.unwrap(ox).equals(mr)) {
deleteMe = ox;
}
}
if (deleteMe!=null) {
theList.remove(deleteMe);
}
} catch (ClassCastException e) {
log.error(e.getMessage(), e);
}
rt.ends.remove(o);
break;
}
}
}
}
}
// The below also renumbers bookmarks, so
// that they are unique across documents.
// Don't need to worry about refs to bookmarks,
// since these are by name, not ID. eg
// <w:instrText xml:space="preserve"> REF MyBookmark </w:instrText>
// except that we want those to be unique
// across documents so prepend doc#_
// for each opening point tag
int counter = 0; // for bookmark renumbering
for (Object o : rt.starts) {
counter++;
// long newId = global + counter; // depending on what global is, these may collide!
long newId = getBookmarkId().getAndIncrement();
if (startIdMethod == null)
startIdMethod = findGetIdMethod(o);
Object id = null;
boolean matched = false;
try {
// BigInteger (eg comment, bookmark)
id = getId(startIdMethod, o);
if (startElement.equals("CTBookmark")) {
Method setIdMethod = findSetIdMethod(o);
if (id instanceof BigInteger) {
setIdMethod.invoke(o, BigInteger.valueOf(newId));
}
// else if (id instanceof String) {
// setIdMethod.invoke(o, "" + newId);
// }
String oldName = ((CTBookmark)o).getName();
String newName = oldName + "_" + instanceNumber ; // can't start with a number
((CTBookmark)o).setName(newName);
for (Object ref : rt.refs) {
Text fieldInstr = (Text)ref;
String fieldVal = fieldInstr.getValue();
if (fieldVal.contains("REF ")
&& fieldVal.contains(" " + oldName + " ") ) {
fieldInstr.setValue(fieldVal.replace(oldName, newName));
}
}
}
// find the closing point tag
BigInteger tmpId;
for (Object end : rt.ends) {
if (endIdMethod == null)
endIdMethod = findGetIdMethod(end);
tmpId = getId(endIdMethod, end);
if (tmpId!=null
&& tmpId.equals(id)) {
// found it
matched = true;
if (endElement.equals("CTMarkupRange")) {
Method setIdMethod = findSetIdMethod(end);
if (id instanceof BigInteger) {
setIdMethod.invoke(end, BigInteger.valueOf(newId));
}
// else if (id instanceof String) {
// setIdMethod.invoke(end, "" + newId);
// }
}
break;
}
}
} catch (ClassCastException cce) {
// String (eg permStart)
id = getIdString(startIdMethod, o);
// if (startElement.equals("CTBookmark")) {
// Method setIdMethod = findSetIdMethod(o);
// if (id instanceof BigInteger) {
// setIdMethod.invoke(o, BigInteger.valueOf(newId));
// }
//// else if (id instanceof String) {
//// setIdMethod.invoke(o, "" + newId);
//// }
// }
// find the closing point tag
String tmpId;
for (Object end : rt.ends) {
if (endIdMethod == null)
endIdMethod = findGetIdMethod(end);
tmpId = getIdString(endIdMethod, end);
if (tmpId!=null
&& tmpId.equals(id)) {
// found it
matched = true;
// if (endElement.equals("CTMarkupRange")) {
// Method setIdMethod = findSetIdMethod(end);
// if (id instanceof BigInteger) {
// setIdMethod.invoke(end, BigInteger.valueOf(newId));
// }
//// else if (id instanceof String) {
//// setIdMethod.invoke(end, "" + newId);
//// }
// }
break;
}
}
}
if (!matched) {
// Object p = paragraphs.get( paragraphs.size() -1 );
// if (p instanceof P) {
// ((P)p).getParagraphContent().add(createObject(endElement,
// id));
// } else {
// System.out.println("add a close tag in " +
// p.getClass().getName() );
// }
/*
* CommentRangeEnd can be block level; Bookmark End can precede
* or follow a w:tbl closing tag.
*
* So for now, insert these at block level. I haven't checked
* the other range tags.
*
* I'm presuming the open tags can be block level as well.
*/
if (endElement.equals("CTMarkupRange")) {
CTMarkupRange mr = Context.getWmlObjectFactory().createCTMarkupRange();
// mr.setId((BigInteger)id);
mr.setId(BigInteger.valueOf(newId));
JAXBElement<CTMarkupRange> bmEnd = Context.getWmlObjectFactory().createBodyBookmarkEnd(mr);
paragraphs.add(bmEnd);
} else
if (endElement.equals("CTPerm")) {
CTPerm mr = Context.getWmlObjectFactory().createCTPerm();
mr.setId((String)id);
JAXBElement<CTPerm> rEnd = Context.getWmlObjectFactory().createBodyPermEnd(mr);
paragraphs.add(rEnd);
} else {
paragraphs.add(createObject(endElement, id));
}
if (refElement != null) {
// In practice this is always CommentReference,
// so rely on that
// if (p instanceof P) {
// R.CommentReference cr =
// Context.getWmlObjectFactory().createRCommentReference();
// cr.setId(id);
// ((P)p).getParagraphContent().add(cr);
// // that should be put in a w:r
// // <w:r><w:rPr><w:rStyle
// w:val="CommentReference"/></w:rPr><w:commentReference
// w:id="0"/></w:r>
// } else {
// System.out.println(" add a close tag in " +
// p.getClass().getName() );
// }
// <w:r><w:rPr><w:rStyle
// w:val="CommentReference"/></w:rPr><w:commentReference
// w:id="0"/></w:r>
P p = Context.getWmlObjectFactory().createP();
R r = Context.getWmlObjectFactory().createR();
p.getParagraphContent().add(r);
R.CommentReference cr = Context.getWmlObjectFactory()
.createRCommentReference();
cr.setId( (BigInteger)id);
r.getRunContent().add(cr);
paragraphs.add(p);
}
}
}
for (Object o : rt.ends) {
counter++;
long newId = getBookmarkId().getAndIncrement();
// only renumber here for ends without starts
if (endIdMethod == null)
endIdMethod = findGetIdMethod(o);
Object id = null;
boolean matched = false;
try {
// BigInteger
id = getId(endIdMethod, o);
// find the opening point tag
BigInteger tmpId;
for (Object start : rt.starts) {
if (startIdMethod == null)
startIdMethod = findGetIdMethod(start);
tmpId = getId(startIdMethod, start);
if (tmpId!=null
&& tmpId.equals(id)) {
// found it
matched = true;
break;
}
}
} catch (ClassCastException cce) {
// String
id = getIdString(endIdMethod, o);
// find the opening point tag
String tmpId;
for (Object start : rt.starts) {
if (startIdMethod == null)
startIdMethod = findGetIdMethod(start);
tmpId = getIdString(startIdMethod, start);
if (tmpId!=null
&& tmpId.equals(id)) {
// found it
matched = true;
break;
}
}
}
if (!matched) {
if (endElement.equals("CTMarkupRange")) {
// missing start, so renumber
Method setIdMethod = findSetIdMethod(o);
if (id instanceof BigInteger) {
setIdMethod.invoke(o, BigInteger.valueOf(newId));
}
}
/* I'm presuming the open tags can be block level as well. */
if (endElement.equals("CTPerm")) {
RangePermissionStart mr = Context.getWmlObjectFactory().createRangePermissionStart();
mr.setId((String)id);
JAXBElement<RangePermissionStart> rs =
Context.getWmlObjectFactory().createBodyPermStart(mr);
paragraphs.add(rs);
} else if (startElement.equals("CTBookmark")) {
log.debug("Add w:bookmarkStart");
Object newObject = createObject(startElement, newId);
paragraphs.add(0, newObject);
if (newObject instanceof CTBookmark) {
// Word complains if a bookmark doesn't have @w:name
String newName = global + "_" + "bookmark" + counter;
((CTBookmark)newObject).setName(newName);
log.info(".. " + newName);
}
} else {
paragraphs.add(0, createObject(startElement, id));
}
}
}
}
private Method findGetIdMethod(Object o) throws Exception {
// Have to do this because getDeclaredMethod
// doesn't find inherited methods, and for bookmarks,
// its inherited
Method[] methods = o.getClass().getMethods();
for (int i=0; i<methods.length; i++) {
if (methods[i].getName().equals("getId")) {
return methods[i];
}
}
log.error("Couldn't find getId for " + o.getClass().getName() );
//(new Throwable()).printStackTrace();
return null;
}
private Method findSetIdMethod(Object o) throws Exception {
Method[] methods = o.getClass().getMethods();
for (int i=0; i<methods.length; i++) {
if (methods[i].getName().equals("setId")) {
return methods[i];
}
}
log.error("Couldn't find setId for " + o.getClass().getName() );
return null;
}
private BigInteger getId(Method idMethod, Object o) throws Exception {
if (idMethod!=null) {
return (BigInteger)idMethod.invoke(o);
}
return null;
}
private static String getIdString(Method idMethod, Object o) throws Exception {
if (idMethod!=null) {
return (String)idMethod.invoke(o);
}
return null;
}
private Object createObject(String name, Object id ) throws Exception {
ObjectFactory factory = Context.getWmlObjectFactory();
log.debug("Looking for method create" + name);
Method method = factory.getClass().getDeclaredMethod("create" + name);
Object newObject = method.invoke(factory);
Method setIdMethod = findSetIdMethod(newObject);
if (setIdMethod==null) {
log.error( "Couldn't findSetIdMethod for " + newObject.getClass().getName());
} else {
log.debug( "FOund findSetIdMethod for " + newObject.getClass().getName());
}
Class param = setIdMethod.getParameterTypes()[0];
setIdMethod.invoke(newObject, convertObject(id, param));
return newObject;
}
private Object convertObject(Object id, Class c) throws Docx4JException {
if (c.isAssignableFrom(id.getClass())) {
return id;
}
if (c==BigInteger.class) {
if (id instanceof Long) {
return BigInteger.valueOf( (Long)id);
}
}
throw new Docx4JException("TODO: Convert " + id.getClass().getName() + " to "+ c.getName() );
}
static class RangeTraverser extends CallbackImpl {
List<Object> starts = new ArrayList<Object>();
List<Object> ends = new ArrayList<Object>();
List<Object> refs = new ArrayList<Object>();
List<CTBookmark> deletes = new ArrayList<CTBookmark>();
String startElement;
String endElement;
String refElement;
RangeTraverser(String startElement, String endElement, String refElement) {
this.startElement = "org.docx4j.wml." + startElement;
this.endElement = "org.docx4j.wml." + endElement;
this.refElement = "org.docx4j.wml." + refElement;
}
@Override
public List<Object> apply(Object o) {
if (o.getClass().getName().equals(startElement)) {
if (o instanceof CTBookmark) { // check for special case
CTBookmark bookmark = (CTBookmark)o;
if (bookmark.getName().equals("_GoBack")) {
deletes.add(bookmark);
} else {
starts.add(o);
}
} else {
starts.add(o);
}
}
if (o.getClass().getName().equals(endElement)) {
ends.add(o);
}
if (o.getClass().getName().equals(refElement)) {
refs.add(o);
} else if (startElement.equals("org.docx4j.wml.CTBookmark")
&& o instanceof javax.xml.bind.JAXBElement
&& ((JAXBElement)o).getName().getLocalPart().equals("instrText")) {
refs.add( XmlUtils.unwrap(o) );
}
return null;
}
}
// public static void main(String[] args) throws Exception {
//
// String input_DOCX = System.getProperty("user.dir") + "/bm_test.docx";
//
// WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage
// .load(new java.io.File(input_DOCX));
//
// BookmarkRenumber.fixRange(
// ((SdtElement)repeated.get(i)).getSdtContent().getContent(),
// "CTBookmark", "CTMarkupRange", null, i);
//
// }
}