001: /*
002: * Copyright 2004 Outerthought bvba and Schaubroeck nv
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package org.outerj.daisy.frontend;
017:
018: import org.apache.cocoon.xml.AbstractXMLPipe;
019: import org.apache.cocoon.xml.XMLConsumer;
020: import org.apache.cocoon.xml.AttributesImpl;
021: import org.apache.cocoon.xml.EmbeddedXMLPipe;
022: import org.xml.sax.Attributes;
023: import org.xml.sax.SAXException;
024: import org.xml.sax.ContentHandler;
025: import org.outerj.daisy.xmlutil.HtmlBodyRemovalHandler;
026:
027: import java.util.*;
028: import java.util.regex.Matcher;
029: import java.util.regex.Pattern;
030:
031: /**
032: * Nests included documents inside their parents.
033: *
034: * <p>The root document should be piped through this SAX handler, the included documents
035: * will then be merged by this handler (recursively), optionally shifting headers
036: * in the included documents.
037: */
038: public class PreparedIncludeHandler extends AbstractXMLPipe {
039: private static final String PUBLISHER_NAMESPACE = "http://outerx.org/daisy/1.0#publisher";
040: private static final Pattern HEADING_PATTERN = Pattern
041: .compile("^h([0-9]+)$");
042: private final PreparedDocuments preparedDocuments;
043: private Map<String, String> startPrefixMappings = new HashMap<String, String>();
044: private Set<String> endPrefixMappings = new HashSet<String>();
045: private int lastEncounteredHeadingLevel = 0;
046: private boolean assumeHtmlBody;
047:
048: /**
049: *
050: * @param assumeHtmlBody true if the prepared documents have html/body tags or false if they are supposed
051: * to contain an embeddable piece of XML (= books vs wiki publishing)
052: */
053: public PreparedIncludeHandler(XMLConsumer consumer,
054: PreparedDocuments preparedDocuments, boolean assumeHtmlBody) {
055: setConsumer(consumer);
056: this .preparedDocuments = preparedDocuments;
057: this .assumeHtmlBody = assumeHtmlBody;
058: }
059:
060: public void startElement(String namespaceURI, String localName,
061: String qName, Attributes atts) throws SAXException {
062: // keep track of current heading level
063: if (namespaceURI.equals("") && localName.startsWith("h")) {
064: String clazz = atts.getValue("class");
065: if (localName.equals("h1") && clazz != null
066: && clazz.indexOf("daisy-document-name") != -1) {
067: lastEncounteredHeadingLevel = 0;
068: } else {
069: Matcher matcher = HEADING_PATTERN.matcher(localName);
070: if (matcher.matches()) {
071: lastEncounteredHeadingLevel = Integer
072: .parseInt(matcher.group(1));
073: }
074: }
075: }
076:
077: if (namespaceURI.equals(PUBLISHER_NAMESPACE)
078: && localName.equals("daisyPreparedInclude")) {
079: int id = Integer.parseInt(atts.getValue("id"));
080: PreparedDocuments.PreparedDocument preparedDocument = preparedDocuments
081: .getPreparedDocument(id);
082:
083: // The prefix mappings which would normally be added to the daisyPreparedInclude element
084: // need to be dropped, otherwise they would be added to the first element of the included
085: // document, which is especially a problem for the default namespace.
086: startPrefixMappings.clear();
087: endPrefixMappings.clear();
088:
089: if (preparedDocument == null) {
090: outputError("Unexpected error in IncludePreparedDocumentsTransformer: missing preparedDocument: "
091: + id);
092: } else {
093: String shiftHeadingsAttr = atts
094: .getValue("shiftHeadings");
095: XMLConsumer consumer = null;
096: if (shiftHeadingsAttr != null) {
097: int shiftHeadingsAmount = 0;
098: boolean shiftHeadingsError = false;
099: if (shiftHeadingsAttr.equals("child")) {
100: shiftHeadingsAmount = lastEncounteredHeadingLevel + 1;
101: } else if (shiftHeadingsAttr.equals("sibling")) {
102: shiftHeadingsAmount = lastEncounteredHeadingLevel;
103: } else {
104: try {
105: shiftHeadingsAmount = Integer
106: .parseInt(shiftHeadingsAttr);
107: if (shiftHeadingsAmount < 0) {
108: shiftHeadingsError = true;
109: }
110: } catch (NumberFormatException e) {
111: shiftHeadingsError = true;
112: }
113: }
114:
115: if (shiftHeadingsError) {
116: outputError("Invalid shiftHeadings specification on include instruction: "
117: + shiftHeadingsAttr);
118: } else {
119: consumer = new HeadingShifter(
120: shiftHeadingsAmount, this );
121: }
122: } else {
123: consumer = this ;
124: }
125:
126: if (consumer != null) {
127: int currentLastHeadingLevel = lastEncounteredHeadingLevel;
128: lastEncounteredHeadingLevel = 0;
129: ContentHandler resultHandler;
130: if (assumeHtmlBody) {
131: resultHandler = new HtmlBodyRemovalHandler(
132: consumer);
133: } else {
134: resultHandler = new EmbeddedXMLPipe(consumer);
135: }
136: preparedDocument.getSaxBuffer()
137: .toSAX(resultHandler);
138: lastEncounteredHeadingLevel = currentLastHeadingLevel;
139: }
140: }
141: } else {
142: emitEndPrefixMappings();
143: emitStartPrefixMappings();
144: super .startElement(namespaceURI, localName, qName, atts);
145: }
146: }
147:
148: private void outputError(String message) throws SAXException {
149: AttributesImpl errorAttrs = new AttributesImpl();
150: errorAttrs.addCDATAAttribute("class", "daisy-error");
151: this .startElement("", "p", "p", errorAttrs);
152: this .characters(message.toCharArray(), 0, message.length());
153: this .endElement("", "p", "p");
154: }
155:
156: public void endElement(String namespaceURI, String localName,
157: String qName) throws SAXException {
158: if (namespaceURI.equals(PUBLISHER_NAMESPACE)
159: && localName.equals("daisyPreparedInclude")) {
160: // ignore
161: } else {
162: super .endElement(namespaceURI, localName, qName);
163: }
164: }
165:
166: public void startPrefixMapping(String prefix, String uri)
167: throws SAXException {
168: startPrefixMappings.put(prefix, uri);
169: }
170:
171: public void endPrefixMapping(String prefix) throws SAXException {
172: if (startPrefixMappings.containsKey(prefix)) {
173: startPrefixMappings.remove(prefix);
174: } else {
175: endPrefixMappings.add(prefix);
176: }
177: }
178:
179: private void emitStartPrefixMappings() throws SAXException {
180: for (Map.Entry<String, String> entry : startPrefixMappings
181: .entrySet()) {
182: super .startPrefixMapping(entry.getKey(), entry.getValue());
183: }
184: startPrefixMappings.clear();
185: }
186:
187: private void emitEndPrefixMappings() throws SAXException {
188: for (String prefix : endPrefixMappings) {
189: super .endPrefixMapping(prefix);
190: }
191: endPrefixMappings.clear();
192: }
193:
194: private static class HeadingShifter extends AbstractXMLPipe {
195: private int shiftHeadingsAmount;
196: private List<EndElementInfo> endElementInfos = new ArrayList<EndElementInfo>();
197:
198: public HeadingShifter(int shiftHeadingsAmount,
199: XMLConsumer consumer) {
200: this .shiftHeadingsAmount = shiftHeadingsAmount;
201: setConsumer(consumer);
202: }
203:
204: public void startElement(String uri, String localName,
205: String qName, Attributes attrs) throws SAXException {
206: if (uri.equals("") && localName.startsWith("h")) {
207: String clazz = attrs.getValue("class");
208: if (localName.equals("h1") && clazz != null
209: && clazz.indexOf("daisy-document-name") != -1) {
210: if (shiftHeadingsAmount > 0) {
211: localName = "h" + shiftHeadingsAmount;
212: qName = localName;
213: AttributesImpl newAttrs = new AttributesImpl(
214: attrs);
215: newAttrs.setValue(newAttrs.getIndex("class"),
216: removeClass(clazz,
217: "daisy-document-name"));
218: attrs = newAttrs;
219: }
220: } else {
221: Matcher matcher = HEADING_PATTERN
222: .matcher(localName);
223: if (matcher.matches()) {
224: int currentLevel = Integer.parseInt(matcher
225: .group(1));
226: localName = "h"
227: + (currentLevel + shiftHeadingsAmount);
228: qName = localName;
229: }
230: }
231: }
232: endElementInfos.add(new EndElementInfo(uri, localName,
233: qName));
234: super .startElement(uri, localName, qName, attrs);
235: }
236:
237: private String removeClass(String classes, String clazz) {
238: int i = classes.indexOf(clazz);
239: if (i != -1) {
240: return classes.substring(0, i)
241: + classes.substring(i + clazz.length());
242: } else {
243: return classes;
244: }
245: }
246:
247: public void endElement(String uri, String loc, String raw)
248: throws SAXException {
249: EndElementInfo endElementInfo = endElementInfos
250: .remove(endElementInfos.size() - 1);
251: super .endElement(endElementInfo.uri,
252: endElementInfo.localName, endElementInfo.qName);
253: }
254:
255: private static class EndElementInfo {
256: private String uri;
257: private String localName;
258: private String qName;
259:
260: public EndElementInfo(String uri, String localName,
261: String qName) {
262: this.uri = uri;
263: this.localName = localName;
264: this.qName = qName;
265: }
266: }
267: }
268: }
|