/*
* Copyright 2004 Outerthought bvba and Schaubroeck nv
*
* Licensed 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.outerj.daisy.frontend;
import org.apache.cocoon.xml.AbstractXMLPipe;
import org.apache.cocoon.xml.XMLConsumer;
import org.apache.cocoon.xml.AttributesImpl;
import org.apache.cocoon.xml.EmbeddedXMLPipe;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ContentHandler;
import org.outerj.daisy.xmlutil.HtmlBodyRemovalHandler;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Nests included documents inside their parents.
*
* <p>The root document should be piped through this SAX handler, the included documents
* will then be merged by this handler (recursively), optionally shifting headers
* in the included documents.
*/
public class PreparedIncludeHandler extends AbstractXMLPipe {
private static final String PUBLISHER_NAMESPACE = "http://outerx.org/daisy/1.0#publisher";
private static final Pattern HEADING_PATTERN = Pattern.compile("^h([0-9]+)$");
private final PreparedDocuments preparedDocuments;
private Map<String, String> startPrefixMappings = new HashMap<String, String>();
private Set<String> endPrefixMappings = new HashSet<String>();
private int lastEncounteredHeadingLevel = 0;
private boolean assumeHtmlBody;
/**
*
* @param assumeHtmlBody true if the prepared documents have html/body tags or false if they are supposed
* to contain an embeddable piece of XML (= books vs wiki publishing)
*/
public PreparedIncludeHandler(XMLConsumer consumer, PreparedDocuments preparedDocuments, boolean assumeHtmlBody) {
setConsumer(consumer);
this.preparedDocuments = preparedDocuments;
this.assumeHtmlBody = assumeHtmlBody;
}
public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
// keep track of current heading level
if (namespaceURI.equals("") && localName.startsWith("h")) {
String clazz = atts.getValue("class");
if (localName.equals("h1") && clazz != null && clazz.indexOf("daisy-document-name") != -1) {
lastEncounteredHeadingLevel = 0;
} else {
Matcher matcher = HEADING_PATTERN.matcher(localName);
if (matcher.matches()) {
lastEncounteredHeadingLevel = Integer.parseInt(matcher.group(1));
}
}
}
if (namespaceURI.equals(PUBLISHER_NAMESPACE) && localName.equals("daisyPreparedInclude")) {
int id = Integer.parseInt(atts.getValue("id"));
PreparedDocuments.PreparedDocument preparedDocument = preparedDocuments.getPreparedDocument(id);
// The prefix mappings which would normally be added to the daisyPreparedInclude element
// need to be dropped, otherwise they would be added to the first element of the included
// document, which is especially a problem for the default namespace.
startPrefixMappings.clear();
endPrefixMappings.clear();
if (preparedDocument == null) {
outputError("Unexpected error in IncludePreparedDocumentsTransformer: missing preparedDocument: " + id);
} else {
String shiftHeadingsAttr = atts.getValue("shiftHeadings");
XMLConsumer consumer = null;
if (shiftHeadingsAttr != null) {
int shiftHeadingsAmount = 0;
boolean shiftHeadingsError = false;
if (shiftHeadingsAttr.equals("child")) {
shiftHeadingsAmount = lastEncounteredHeadingLevel + 1;
} else if (shiftHeadingsAttr.equals("sibling")) {
shiftHeadingsAmount = lastEncounteredHeadingLevel;
} else {
try {
shiftHeadingsAmount = Integer.parseInt(shiftHeadingsAttr);
if (shiftHeadingsAmount < 0) {
shiftHeadingsError = true;
}
} catch (NumberFormatException e) {
shiftHeadingsError = true;
}
}
if (shiftHeadingsError) {
outputError("Invalid shiftHeadings specification on include instruction: " + shiftHeadingsAttr);
} else {
consumer = new HeadingShifter(shiftHeadingsAmount, this);
}
} else {
consumer = this;
}
if (consumer != null) {
int currentLastHeadingLevel = lastEncounteredHeadingLevel;
lastEncounteredHeadingLevel = 0;
ContentHandler resultHandler;
if (assumeHtmlBody) {
resultHandler = new HtmlBodyRemovalHandler(consumer);
} else {
resultHandler = new EmbeddedXMLPipe(consumer);
}
preparedDocument.getSaxBuffer().toSAX(resultHandler);
lastEncounteredHeadingLevel = currentLastHeadingLevel;
}
}
} else {
emitEndPrefixMappings();
emitStartPrefixMappings();
super.startElement(namespaceURI, localName, qName, atts);
}
}
private void outputError(String message) throws SAXException {
AttributesImpl errorAttrs = new AttributesImpl();
errorAttrs.addCDATAAttribute("class", "daisy-error");
this.startElement("", "p", "p", errorAttrs);
this.characters(message.toCharArray(), 0, message.length());
this.endElement("", "p", "p");
}
public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
if (namespaceURI.equals(PUBLISHER_NAMESPACE) && localName.equals("daisyPreparedInclude")) {
// ignore
} else {
super.endElement(namespaceURI, localName, qName);
}
}
public void startPrefixMapping(String prefix, String uri) throws SAXException {
startPrefixMappings.put(prefix, uri);
}
public void endPrefixMapping(String prefix) throws SAXException {
if (startPrefixMappings.containsKey(prefix)) {
startPrefixMappings.remove(prefix);
} else {
endPrefixMappings.add(prefix);
}
}
private void emitStartPrefixMappings() throws SAXException {
for (Map.Entry<String, String> entry : startPrefixMappings.entrySet()) {
super.startPrefixMapping(entry.getKey(), entry.getValue());
}
startPrefixMappings.clear();
}
private void emitEndPrefixMappings() throws SAXException {
for (String prefix : endPrefixMappings) {
super.endPrefixMapping(prefix);
}
endPrefixMappings.clear();
}
private static class HeadingShifter extends AbstractXMLPipe {
private int shiftHeadingsAmount;
private List<EndElementInfo> endElementInfos = new ArrayList<EndElementInfo>();
public HeadingShifter(int shiftHeadingsAmount, XMLConsumer consumer) {
this.shiftHeadingsAmount = shiftHeadingsAmount;
setConsumer(consumer);
}
public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
if (uri.equals("") && localName.startsWith("h")) {
String clazz = attrs.getValue("class");
if (localName.equals("h1") && clazz != null && clazz.indexOf("daisy-document-name") != -1) {
if (shiftHeadingsAmount > 0) {
localName = "h" + shiftHeadingsAmount;
qName = localName;
AttributesImpl newAttrs = new AttributesImpl(attrs);
newAttrs.setValue(newAttrs.getIndex("class"), removeClass(clazz, "daisy-document-name"));
attrs = newAttrs;
}
} else {
Matcher matcher = HEADING_PATTERN.matcher(localName);
if (matcher.matches()) {
int currentLevel = Integer.parseInt(matcher.group(1));
localName = "h" + (currentLevel + shiftHeadingsAmount);
qName = localName;
}
}
}
endElementInfos.add(new EndElementInfo(uri, localName, qName));
super.startElement(uri, localName, qName, attrs);
}
private String removeClass(String classes, String clazz) {
int i = classes.indexOf(clazz);
if (i != -1) {
return classes.substring(0, i) + classes.substring(i + clazz.length());
} else {
return classes;
}
}
public void endElement(String uri, String loc, String raw) throws SAXException {
EndElementInfo endElementInfo = endElementInfos.remove(endElementInfos.size() - 1);
super.endElement(endElementInfo.uri, endElementInfo.localName, endElementInfo.qName);
}
private static class EndElementInfo {
private String uri;
private String localName;
private String qName;
public EndElementInfo(String uri, String localName, String qName) {
this.uri = uri;
this.localName = localName;
this.qName = qName;
}
}
}
}
|