/*
* Copyright (C) 2010 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.wikbook.core.model.content.block;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.wikbook.core.model.ElementContainer;
import org.wikbook.core.model.DocbookBuilderContext;
import org.wikbook.core.ResourceType;
import org.wikbook.core.WikbookException;
import org.wikbook.core.codesource.CodeContext;
import org.wikbook.core.codesource.CodeProcessor;
import org.wikbook.core.model.DocbookElement;
import org.wikbook.core.xml.OutputFormat;
import org.wikbook.core.xml.XML;
import org.wikbook.text.Position;
import org.wikbook.text.TextArea;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
* @version $Revision$
*/
public class ProgramListingElement extends BlockElement
{
/** . */
private final ElementContainer<CalloutElement> callouts;
/** . */
private final LanguageSyntax languageSyntax;
/** . */
private final Integer indent;
/** . */
private final String content;
/** . */
private final DocbookBuilderContext context;
/** . */
private final boolean highlightCode;
/** . */
private String listing;
/** . */
private final DocumentBuilder documentBuilder;
/** . */
private final XPath xpath;
public ProgramListingElement(
final DocbookBuilderContext context,
LanguageSyntax languageSyntax,
Integer indent,
String content,
boolean highlightCode)
{
//
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setIgnoringElementContentWhitespace(true);
dbf.setCoalescing(true);
dbf.setNamespaceAware(true);
dbf.setXIncludeAware(false);
//
DocumentBuilder documentBuilder;
try
{
documentBuilder = dbf.newDocumentBuilder();
}
catch (ParserConfigurationException e)
{
throw new WikbookException(e);
}
//
this.context = context;
this.languageSyntax = languageSyntax;
this.indent = indent;
this.content = content;
this.highlightCode = highlightCode;
this.callouts = new ElementContainer<CalloutElement>(CalloutElement.class);
this.documentBuilder = documentBuilder;
this.xpath = XPathFactory.newInstance().newXPath();
}
public String getListing()
{
return listing;
}
public LanguageSyntax getLanguageSyntax()
{
return languageSyntax;
}
public boolean isHighlightCode()
{
return highlightCode;
}
public ElementContainer<CalloutElement> getCallouts()
{
return callouts;
}
private void performIncludes(Element elt)
{
if ("urn:wikbook:internal".equals(elt.getNamespaceURI()) && "include".equals(elt.getLocalName()))
{
String hrefAttr = elt.getAttribute("href");
List<Node> replacementElts = null;
try
{
URL url = assertContextualized().resolveResource(ResourceType.XML, hrefAttr);
if (url != null)
{
Document includedDoc = documentBuilder.parse(url.openStream());
//
if (elt.hasAttribute("xpath"))
{
String xpathAttr = elt.getAttribute("xpath");
XPathExpression xpathExpr = xpath.compile(xpathAttr);
replacementElts = new ArrayList<Node>();
NodeList resolvedNodes = (NodeList)xpathExpr.evaluate(includedDoc, XPathConstants.NODESET);
for (int i = 0;i < resolvedNodes.getLength();i++)
{
Node resolvedNode = resolvedNodes.item(i);
if (resolvedNode.getNodeType() != Document.ATTRIBUTE_NODE)
{
replacementElts.add(elt.getOwnerDocument().importNode(resolvedNode, true));
}
else
{
// Log that somehow
// throw new Exception("Target of xpath expression " + xpathAttr + " does not resolve to an XML element but to " + resolvedNode);
}
}
}
else
{
Element includedElt = includedDoc.getDocumentElement();
replacementElts = Collections.singletonList(elt.getOwnerDocument().importNode(includedElt, true));
}
}
}
catch (Exception e)
{
context.onValidationError(e);
Element errorElt = elt.getOwnerDocument().createElement("wikbook:error");
Text text = elt.getOwnerDocument().createTextNode(e.getMessage());
errorElt.appendChild(text);
replacementElts = Collections.<Node>singletonList(errorElt);
}
// Insert elements
for (Node replacement : replacementElts)
{
elt.getParentNode().insertBefore(replacement, elt);
}
// Remove include
elt.getParentNode().removeChild(elt);
}
else
{
for (Element childElt : XML.elements(elt))
{
performIncludes(childElt);
}
}
}
public void process()
{
String bilto;
switch (languageSyntax)
{
case XML:
try
{
// Wrap the whole document with a root and declares the internal wikbook namespace
String data = "<root xmlns:wikbook=\"urn:wikbook:internal\">" + content.replaceAll("<\\?xml.*\\?>", "") + "</root>";
// Parse the resulting document
Document doc = documentBuilder.parse(new InputSource(new StringReader(data)));
Element docElt = doc.getDocumentElement();
// Perform inclusions
performIncludes(docElt);
// Remove white spaces
// todo : check if we can do it in the {@link DocumentFormatterFilter} directly
if (indent != null)
{
XML.removeWhiteSpace(docElt);
}
// The output buffer
StringWriter writer = new StringWriter();
// Create transformer handler that will serialize to a steam
TransformerHandler serializer = XML.createTransformerHandler(new OutputFormat(indent, false));
// Set the result that will receive the stream
serializer.setResult(new StreamResult(writer));
// This transformer will transform a DOM into a serie of sax events
Transformer domToSAX = TransformerFactory.newInstance().newTransformer();
// Now transform the DOM into a serie of filtered event to the stream receiver
domToSAX.transform(new DOMSource(doc), new SAXResult(new DocumentFormatterFilter(serializer)));
// Get the original data
data = writer.toString();
//
TextArea ta = new TextArea(data);
ta.trimLeft();
//
bilto = ta.getText();
}
catch (Exception e)
{
context.onValidationError(e);
bilto = "Exception occured, see logs";
}
break;
case JAVA:
CodeContextImpl ctx = new CodeContextImpl();
//
new CodeProcessor().parse(content, ctx);
// Create the resulting callouts
for (Map.Entry<String, Callout> callout : ctx.callouts.entrySet())
{
if (callout.getValue().text != null)
{
CalloutElement calloutElt = new CalloutElement(callout.getValue().ids, callout.getValue().text);
//
assertContextualized().build(new StringReader(callout.getValue().text), calloutElt);
//
merge(calloutElt);
}
}
//
bilto = ctx.sb.toString();
//
break;
default:
bilto = content;
break;
}
//
this.listing = bilto;
}
@Override
public boolean append(DocbookElement elt)
{
return callouts.append(elt);
}
/** . */
private static final WeakHashMap<DocbookBuilderContext, AtomicInteger> sequences = new WeakHashMap<DocbookBuilderContext, AtomicInteger>();
private class CodeContextImpl implements CodeContext
{
/** . */
private final StringBuilder sb = new StringBuilder();
/** . */
private final TreeMap<String, Callout> callouts = new TreeMap<String, Callout>();
private CodeContextImpl()
{
}
public void writeContent(String content)
{
sb.append(content);
}
public void writeCallout(String index)
{
DocbookBuilderContext key = ProgramListingElement.this.context;
AtomicInteger sequence = sequences.get(key);
if (sequence == null)
{
sequences.put(key, sequence = new AtomicInteger());
}
String coId = "callout-" + sequence.getAndIncrement();
//
TextArea ta = new TextArea(sb.toString());
Position pos = ta.position(sb.length());
//
Callout callout = callouts.get(index);
if (callout == null)
{
callout = new Callout();
callouts.put(index, callout);
}
// Write a white space so we are sure that the position for the callout exist in the text
writeContent(" ");
//
callout.ids.put(coId, pos);
}
public void setCallout(String index, String text)
{
Callout callout = callouts.get(index);
if (callout == null)
{
callout = new Callout();
callouts.put(index, callout);
}
callout.text = text;
}
public InputStream resolveResources(String id) throws IOException
{
List<URL> list = context.resolveResources(ResourceType.JAVA, id);
if (list.size() > 0)
{
return list.get(0).openStream();
}
return null;
}
}
private static class Callout
{
/** . */
private String text;
/** . */
private final LinkedHashMap<String, Position> ids = new LinkedHashMap<String, Position>();
}
}