com.fluidops.iwb.wiki.FluidWikiModel.java Source code

Java tutorial

Introduction

Here is the source code for com.fluidops.iwb.wiki.FluidWikiModel.java

Source

/*
 * Copyright (C) 2008-2013, fluid Operations AG
 *
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package com.fluidops.iwb.wiki;

import static info.bliki.wiki.template.AbstractTemplateFunction.parseTrim;
import info.bliki.htmlcleaner.ContentToken;
import info.bliki.htmlcleaner.TagNode;
import info.bliki.wiki.filter.WikipediaParser;
import info.bliki.wiki.model.Configuration;
import info.bliki.wiki.model.ImageFormat;
import info.bliki.wiki.model.WikiModel;
import info.bliki.wiki.tags.HTMLBlockTag;
import info.bliki.wiki.tags.WPATag;
import info.bliki.wiki.tags.util.TagStack;
import info.bliki.wiki.template.ITemplateFunction;

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.axis.utils.StringUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.log4j.Logger;
import org.openrdf.model.Literal;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.impl.ValueFactoryImpl;

import com.fluidops.ajax.components.FComponent;
import com.fluidops.iwb.api.EndpointImpl;
import com.fluidops.iwb.api.NamespaceService;
import com.fluidops.iwb.api.ReadDataManager;
import com.fluidops.iwb.cms.util.IWBCmsUtil;
import com.fluidops.iwb.user.UserManager;
import com.fluidops.iwb.user.UserManager.ValueAccessLevel;
import com.fluidops.iwb.util.Config;
import com.fluidops.iwb.widget.AbstractWidget;
import com.fluidops.iwb.widget.Widget;
import com.fluidops.iwb.wiki.parserfunction.IWBMagicWords;
import com.fluidops.iwb.wiki.parserfunction.PageContextAwareParserFunction;
import com.fluidops.iwb.wiki.parserfunction.ParserFunctionUtil;

import edu.umd.cs.findbugs.annotations.SuppressWarnings;

/**
 * The IWikiModel model implementation for FluidWiki.
 * 
 * This extends the original WikiModel, but needs to override various
 * methods to get customized functionality, like unit conversion, support
 * for Wiki templates, etc.
 * 
 * @author Uli
 */
public class FluidWikiModel extends WikiModel {
    private static final Logger logger = Logger.getLogger(FluidWikiModel.class.getName());

    /**
     * List of custom template resolvers.
     */
    List<TemplateResolver> templateResolver = new ArrayList<TemplateResolver>();

    //   /**
    //    * The number of occurrences of every wiki link. Useful when having to
    //    * annotate a link which occurs several times on a page.
    //    */
    //   private HashMap<String, Integer> wikiLinkOccurrences = new HashMap<String, Integer>();
    //
    //   /**
    //    * Needed in the wiki text parsing phase. Temporarily stores the predicate
    //    * for the current link.
    //    * 
    //    * @see FluidWikiModel#addSemanticRelation(String, String)
    //    */
    //   private String tempPredicate = "";

    private URI page;

    private Set<String> included = new HashSet<String>();

    private FComponent parent;

    public static final String WIKIPEDIA_IMAGE_ROOT = "http://upload.wikimedia.org/wikipedia";

    private static final Charset UTF8 = Charset.forName("UTF-8");

    static ThreadLocal<List<FComponent>> renderedComponents = new ThreadLocal<List<FComponent>>();
    static ThreadLocal<List<Class<? extends AbstractWidget<?>>>> parsedWidgets = new ThreadLocal<List<Class<? extends AbstractWidget<?>>>>();

    // Keeps image links so that they can later be checked with WikiDiagnosticsWidget
    private Set<String> imageLinks = new HashSet<String>();

    /**
     * Configuration setting for the engine: if set to true, {@link IWBMagicWords} are processed
     * Can be set via {@link #setReplaceIWBMagicWords(boolean)}
     */
    private boolean replaceIWBMagicWords = true;

    private static final ThreadLocal<MessageDigest> localDigest = new ThreadLocal<MessageDigest>() {
        protected MessageDigest initialValue() {
            try {
                return MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                logger.warn("Initial value", e);
                throw new RuntimeException(e);
            }
        }
    };

    static {
        //allow html img tags
        Configuration.DEFAULT_CONFIGURATION.addTokenTag("img", new ImgTag());
        HTMLBlockTag.addAllowedAttribute(Widget.WIDGET_ATTRIBUTE);
        HTMLBlockTag.addAllowedAttribute("mode");
        TagNode.addAllowedAttribute("style");
        TagNode.addAllowedAttribute("target");
        // TagNode.addAllowedAttribute("cht");
        // TagNode.addAllowedAttribute("chd");
        // TagNode.addAllowedAttribute("chs");
        // TagNode.addAllowedAttribute("chl");
        // TagNode.addAllowedAttribute("url");
        // TagNode.addAllowedAttribute("ci");
        // TagNode.addAllowedAttribute("ca");
        // TagNode.addAllowedAttribute("xrange");
        // TagNode.addAllowedAttribute("yrange");
        // TagNode.addAllowedAttribute("function");
        // TagNode.addAllowedAttribute("d");
        // TagNode.addAllowedAttribute("expr");
    }

    /**
     * Creates a new instance
     */
    public FluidWikiModel(URI page) {
        this(page, null);
    }

    public FluidWikiModel(URI page, FComponent parent) {
        super("/upload/${image}", "${title}");
        this.page = page;
        this.parent = parent;
    }

    /**
     * Enable / disable the processing of {@link IWBMagicWords}
     * 
     * @param replaceIWBMagicWords the replaceIWBMagicWords to set
     */
    public void setReplaceIWBMagicWords(boolean replaceIWBMagicWords) {
        this.replaceIWBMagicWords = replaceIWBMagicWords;
    }

    /**
     * Override and enable semantic web features.
     */
    @Override
    public boolean isSemanticWebActive() {
        return Config.getConfig().getSemanticWikiLinksEnabled();
    }

    /**
     * Render now uses our own customized HTML converter.
     */
    @Override
    public String render(String wikiText) {
        return render(new FluidHTMLConverter(), wikiText);
    }

    private Map<String, PageContextAwareParserFunction> contextAwareTemplateFunctions = new HashMap<String, PageContextAwareParserFunction>();

    /**
     * Add a context aware parser function. The name is taken from
     * {@link PageContextAwareParserFunction#getFunctionName()}
     * @param pf
     */
    public void addContextAwareTemplateFunction(PageContextAwareParserFunction pf) {
        contextAwareTemplateFunctions.put(pf.getFunctionName(), pf);
    }

    @Override
    public ITemplateFunction getTemplateFunction(String name) {
        // return context aware parser functions first, then return default implementations
        if (contextAwareTemplateFunctions.containsKey(name))
            return contextAwareTemplateFunctions.get(name);
        return super.getTemplateFunction(name);
    }

    /**
     * Compute the location of a (logical) image.
     * 
     * @param imageName
     * @return
     */
    private static String computeImagePath(String imageName) {
        imageName = imageName.replace(' ', '_');
        int first = localDigest.get().digest(imageName.getBytes(UTF8))[0] & 0xff;
        String hash = Integer.toHexString(first).toLowerCase(Locale.US);
        return hash.charAt(0) + "/" + hash + "/" + imageName;
    }

    @Override
    public void substituteTemplateCall(String templateName, Map<String, String> parameterMap, Appendable writer)
            throws IOException {

        if (!included.contains(templateName)) {
            included.add(templateName);
            super.substituteTemplateCall(templateName, parameterMap, writer);
            included.remove(templateName);
        } else
            writer.append("(Error: infinite loop of template " + templateName + ")");
    }

    @Override
    public void parseInternalImageLink(String imageNamespace, String rawImageLink) {

        this.imageLinks.add(rawImageLink);

        String imageLocation = "";

        if (getImageBaseURL() != null) {
            ImageFormat imageFormat = ImageFormat.getImageFormat(rawImageLink, imageNamespace);

            // Uli: make sure we detect if an image has no location set
            if ("none".equals(imageFormat.getLocation())) {
                if (rawImageLink.indexOf("|none") < 0)
                    imageFormat.setLocation("unset");
            }

            String imageName = imageFormat.getFilename();

            // Default image width for thumbnail
            if (imageFormat.getType() != null && imageFormat.getType().equalsIgnoreCase("thumb")
                    && imageFormat.getWidth() <= 0)
                imageFormat.setSize("180px");

            if (replaceColon())
                imageName = imageName.replaceAll(":", "/");

            try {
                imageLocation = getImageLocation(imageFormat, imageName);
                if (imageLocation.contains("..")) {
                    appendErrorMessage("Invalid internal link containing '..' sequence: " + rawImageLink
                            + ". Only links to uploaded files are allowed.");
                } else {
                    String thumbLocation = getImageThumbLocation(imageFormat, imageName, imageLocation);
                    String href = EndpointImpl.api().getRequestMapper()
                            .getRequestStringFromValue(ValueFactoryImpl.getInstance().createURI(
                                    EndpointImpl.api().getNamespaceService().fileNamespace(), imageName));
                    appendInternalImageLink(href, thumbLocation, imageFormat);
                }
            } catch (RuntimeException e) {
                logger.debug("Error while parsing internal image link: ", e);
                throw e;
            }

        }

    }

    private String getImageLocation(ImageFormat imageFormat, String imageName) {
        // TODO: we should encapsulate this logics

        // ... check if the image is at an absolute path:
        if (imageName.startsWith("http://") || imageName.startsWith("https://"))
            return imageName;

        // for dbpedia, we have special handling
        if (page.getNamespace().equals("http://dbpedia.org/resource/") && !page.getLocalName().contains("redirect")
                && !page.getLocalName().contains("Template")) {
            String imagePath = computeImagePath(imageName);

            // heuristics for image path in wikipedia.
            String imageLocation = WIKIPEDIA_IMAGE_ROOT + "/commons/" + imagePath;
            return imageLocation;

            // the following heuristic is based on the assumption that images
            // reside in two different domains. The problem with this heuristic
            // is that we send a large amount of requests and produce errors if
            // the image is not available at all. By using the simplified heuristic
            // we avoid errors and let the browser deal with it. Note: few pictures
            // from the "en" domain will not be displayed, e.g. on Barack_Obama     
            // It seems that images are either in the commmons or in the en domain
            // see bug 8277
            //           String[] possibleDomains = new String[]{"commons", "en"};           
            //           for (String domain: possibleDomains)
            //           {
            //               String imageLocation = WIKIPEDIA_IMAGE_ROOT + "/" + domain + "/" + imagePath;
            //               if (fileExists(imageLocation))
            //                   return imageLocation;
            //           }
        }

        // ... and, in every other case interpret the image as local image
        return IWBCmsUtil.getAccessUrl(imageName);
    }

    private String getImageThumbLocation(ImageFormat imageFormat, String imageName, String imageLocation) {
        // TODO: we should encapsulate this logics
        // for dbpedia, we have special handling
        if (page.getNamespace().equals("http://dbpedia.org/resource/") && !page.getLocalName().contains("redirect")
                && !page.getLocalName().contains("Template")) {
            String domain = "";
            // Considering the Wikipedia base URL
            if (!StringUtils.isEmpty(imageLocation))
                domain = imageLocation.split("/")[4];

            if (StringUtils.isEmpty(domain))
                return "";

            String imagePath = computeImagePath(imageName);

            String thumbLocation = WIKIPEDIA_IMAGE_ROOT;
            thumbLocation += "/" + domain + "/thumb/" + imagePath + "/" + imageFormat.getWidthStr() + "-"
                    + imageName;

            if (imageName.endsWith(".svg"))
                thumbLocation += ".png";

            thumbLocation = thumbLocation.replace(' ', '_');

            return thumbLocation;
        }

        // if the page is not in the dbpedia namespace...
        // ... check if the image is at an absolute path:
        // TODO: Linking to external files in this way is non-standard notation and should not be supported
        if (imageName.startsWith("http://") || imageName.startsWith("https://"))
            return imageName;
        // ... and, if not, interpret the image as local image
        else
            return imageLocation;
    }

    /**
     * Template resolver interface. Used to hook custom templates.
     * 
     * @author Uli
     */
    public static interface TemplateResolver {
        public String resolveTemplate(String namespace, String templateName, Map<String, String> templateParameters,
                URI page, FComponent parent);
    }

    /**
     * Adds the given template resolver.
     * 
     * @param e
     */
    public void addTemplateResolver(TemplateResolver e) {
        templateResolver.add(e);
    }

    /**
     * Removes the previously added template resolver.
     * 
     * @param e
     */
    public void removeTemplateResolver(TemplateResolver e) {
        templateResolver.remove(e);
    }

    /**
     * Constructs the template string for the given args.
     * @param namespace
     * @param templateName
     * @param templateParameters
     * @return
     */
    @SuppressWarnings(value = "SBSC_USE_STRINGBUFFER_CONCATENATION", justification = "Checked")
    String templateToString(String namespace, String templateName, Map<String, String> templateParameters) {
        String res = namespace + ":" + templateName;
        for (int arg = 1;; arg++) {
            if (templateParameters == null)
                break;
            String textArg = templateParameters.get("" + arg);

            if (textArg == null)
                break;
            res += "|" + textArg;
        }
        return res;
    }

    /**
     * @see {@link http://code.google.com/p/gwtwiki/wiki/WikiModels}
     */
    @Override
    public String getRawWikiContent(String namespace, String templateName, Map<String, String> templateParameters) {
        if (templateName.equalsIgnoreCase("FixBunching"))
            return "";

        // process platform specific magic words
        if (replaceIWBMagicWords && IWBMagicWords.isMagicWord(templateName)) {
            try {
                return IWBMagicWords.processMagicWord(templateName, "", this, page);
            } catch (Exception e) {
                logger.debug("Error processing magic word '" + templateName + "':", e);
                return ParserFunctionUtil.renderError(e);
            }
        }

        // Use default template resolver (for {{CURRENTDAY}} etc.)
        String result = super.getRawWikiContent(namespace, templateName, templateParameters);
        if (result != null)
            return result;

        // Translate {{reflist}} to the current <references/>
        if (templateName.equalsIgnoreCase("reflist"))
            return "<references/>";
        // Quick and dirty "cite web / book / ..." implementation
        else if (templateName.toLowerCase().startsWith("cite")) {
            String url = templateParameters.get("url");
            String title = templateParameters.get("title");
            // String date = templateParameters.get("date");

            if (title != null)
                return "[[" + (url != null ? url + " | " : "") + title + "]]";
        }
        // Support unit conversion
        else if (templateName.equalsIgnoreCase("convert")) {
            // {{convert|115|km|mi|lk=on|abbr=on}}
            String n = templateParameters.get("1");
            String unit = templateParameters.get("2");
            String unit2 = templateParameters.get("3");

            UnitInfo ui1 = getUnitInfo(unit);
            UnitInfo ui2 = getUnitInfo(unit2);
            if (ui1 != null && ui2 != null) {
                double dn = Double.parseDouble(n.replace(",", "."));
                double n2 = dn * ui1.factor / ui2.factor;

                String n2s = String.format("%.2f", n2);

                return n + " [[" + ui1.full + "|" + unit + "]] (" + n2s + " [[" + ui2.full + "|" + unit2 + "]])";
            } else
                return n + " " + unit;
        }
        // Try to resolve the template using custom resolvers (if any)
        else
            for (TemplateResolver tr : templateResolver) {
                String res = tr.resolveTemplate(namespace, templateName, templateParameters, page, parent);
                if (res != null)
                    return res;
            }

        return "";
    }

    /**
     * Method for processing an image link.
     * Here we can customize and prepare args for the HTML converter.
     */
    @Override
    public void appendInternalImageLink(String hrefImageLink, String srcImageLink, ImageFormat imageFormat) {
        int pxWidth = imageFormat.getWidth();
        int pxHeight = imageFormat.getHeight();
        String caption = imageFormat.getCaption();
        TagNode divTagNode = new TagNode("div");
        divTagNode.addAttribute("id", "image", false);

        // String link = imageFormat.getLink();
        // if (link != null) {
        // String href = encodeTitleToUrl(link, true);
        // divTagNode.addAttribute("href", href, false);
        // } else {
        if (hrefImageLink.length() != 0) {
            divTagNode.addAttribute("href", hrefImageLink, false);
        }
        // }
        divTagNode.addAttribute("src", srcImageLink, false);
        divTagNode.addObjectAttribute("wikiobject", imageFormat);
        if (pxHeight != -1) {
            if (pxWidth != -1) {
                divTagNode.addAttribute("style", "height:" + pxHeight + "px; " + "width:" + pxWidth + "px", false);
            } else {
                divTagNode.addAttribute("style", "height:" + pxHeight + "px", false);
            }
        } else {
            if (pxWidth != -1) {
                divTagNode.addAttribute("style", "width:" + pxWidth + "px", false);
            }
        }
        pushNode(divTagNode);

        String imageType = imageFormat.getType();
        // TODO: test all these cases
        if (caption != null && caption.length() > 0
                && ("frame".equals(imageType) || "thumb".equals(imageType) || "thumbnail".equals(imageType))) {

            TagNode captionTagNode = new TagNode("div");
            String clazzValue = "caption";
            String type = imageFormat.getType();
            if (type != null) {
                clazzValue = type + clazzValue;
            }
            captionTagNode.addAttribute("class", clazzValue, false);
            //         
            TagStack localStack = WikipediaParser.parseRecursive(caption, this, true, true);
            captionTagNode.addChildren(localStack.getNodeList());
            String altAttribute = imageFormat.getAlt();
            if (altAttribute == null) {
                altAttribute = captionTagNode.getBodyString();
                imageFormat.setAlt(altAttribute);
            }
            pushNode(captionTagNode);
            // WikipediaParser.parseRecursive(caption, this);
            popNode();
        }

        popNode(); // div
    }

    /*
     * We need to override the logic for extracting semantic relations / attributes, 
     * as Bliki only supports the old distinction via :: vs. :=
     */
    @Override
    public boolean appendRawNamespaceLinks(String rawNamespaceTopic, String viewableLinkDescription,
            boolean containsNoPipe) {

        int colonIndex = rawNamespaceTopic.indexOf("::");
        int colonEqualsIndex = rawNamespaceTopic.indexOf(":=");

        if (!this.isSemanticWebActive())
            return false;

        if (colonIndex != -1) {
            String nameSpace = rawNamespaceTopic.substring(0, colonIndex);

            if (rawNamespaceTopic.length() > colonIndex + 1) {
                // See http://en.wikipedia.org/wiki/Semantic_MediaWiki for more information.
                if (rawNamespaceTopic.charAt(colonIndex + 1) == ':') {
                    // found an SMW relation
                    String relationValue = rawNamespaceTopic.substring(colonIndex + 2);

                    Value object = null;

                    ReadDataManager dm = EndpointImpl.api().getDataManager();

                    NamespaceService ns = EndpointImpl.api().getNamespaceService();

                    URI predicate = ns.guessURI(nameSpace);

                    if (predicate != null) {
                        object = dm.guessValueForPredicate(relationValue, ns.guessURI(nameSpace), true);

                        if (object instanceof URI) {
                            if (addSemanticRelation(nameSpace, relationValue)) {
                                if (containsNoPipe)
                                    viewableLinkDescription = dm.getLabel(object);
                                if (viewableLinkDescription.trim().length() > 0)
                                    appendInternalLink(relationValue, null, viewableLinkDescription, "interwiki",
                                            false);
                                return true;
                            }
                        } else if (object instanceof Literal) {
                            if (addSemanticAttribute(nameSpace, relationValue)) {
                                append(new ContentToken(relationValue));
                                return true;
                            }
                        }
                    }
                    // in all other cases: predicate==null || object==null 
                    // -> no parsable URI, so the best we can do here is to ignore it
                }
            }

            if (isCategoryNamespace(nameSpace)) {
                // add the category to this texts metadata
                String category = rawNamespaceTopic.substring(colonIndex + 1).trim();
                if (category != null && category.length() > 0) {
                    // TODO implement more sort-key behaviour
                    // http://en.wikipedia.org/wiki/Wikipedia:Categorization#
                    // Category_sorting
                    addCategory(category, viewableLinkDescription);
                    return true;
                }
            } else if (isInterWiki(nameSpace)) {
                String title = rawNamespaceTopic.substring(colonIndex + 1);
                if (title != null && title.length() > 0) {
                    appendInterWikiLink(nameSpace, title, viewableLinkDescription);
                    return true;
                }
            }
        } else if (colonEqualsIndex != -1) {
            // found an SMW attribute
            String nameSpace = rawNamespaceTopic.substring(0, colonEqualsIndex);

            String attributeValue = rawNamespaceTopic.substring(colonEqualsIndex + 2);
            if (addSemanticAttribute(nameSpace, attributeValue)) {
                append(new ContentToken(StringEscapeUtils.escapeHtml(attributeValue)));
                return true;
            }
        }
        return false;
    }

    /**
     * This method is only called when parsing an annotated semantic link. It
     * basically tells us that the "currently processed" link has a predicate.
     * So the next time
     * {@link #appendInternalLink(String, String, String, String, boolean)} is
     * called, it is called on a link with predicate "relation".
     */
    public boolean addSemanticRelation(String relation, String relationValue) {
        // Store current predicate
        //      tempPredicate = relation;

        return super.addSemanticRelation(relation, relationValue);
    }

    /**
     * Returns the meta info about a unit
     * 
     * @param u
     * @return
     */
    static UnitInfo getUnitInfo(String u) {
        for (UnitInfo ui : lengthUnits)
            if (ui.abbrev.equals(u) || ui.full.equals(u))
                return ui;
        return null;
    }

    /**
     * Class that represent unit meta info
     * 
     * @author Uli
     * 
     */
    static class UnitInfo {
        UnitInfo(String abbrev, String full, double factor) {
            this.abbrev = abbrev;
            this.full = full;
            this.factor = factor;
        }

        String abbrev;
        String full;
        double factor;
    }

    /**
     * All known units
     */
    final static UnitInfo[] lengthUnits = { new UnitInfo("mm", "meter", 0.001),
            new UnitInfo("cm", "centimeter", 0.01), new UnitInfo("dm", "decimeter", 0.1),
            new UnitInfo("m", "meter", 1.0), new UnitInfo("km", "kilometer", 1000.0),
            new UnitInfo("mi", "mile", 1000.0 * 1.609344), new UnitInfo("in", "inch", 0.01 * 2.54),
            new UnitInfo("km/h", "kilometers per hour", 1000.0),
            new UnitInfo("mph", "miles per hour", 1000.0 * 1.609344) };

    /**
     * Initializes the static part of the rendering model.
     */
    public static void initModel() {
        renderedComponents.set(new ArrayList<FComponent>());
        parsedWidgets.set(new ArrayList<Class<? extends AbstractWidget<?>>>());
    }

    /**
     * Returns the list of parsed widget classes.
     * @return
     */
    public static List<Class<? extends AbstractWidget<?>>> getParsedWidgets() {
        return parsedWidgets.get();
    }

    /**
     * Returns the list of rendered components.
     * @return
     */
    public static List<FComponent> getRenderedComponents() {
        return renderedComponents.get();
    }

    /**
     * Add the rendered component.
     */
    public static void addRenderedComponent(FComponent f) {
        renderedComponents.get().add(f);
    }

    /**
     * Add a parsed widget
     */
    public static void addParsedWidget(Class<? extends AbstractWidget<?>> c) {
        parsedWidgets.get().add(c);
    }

    public String addInterwikiLink(String key, String value) {
        return super.addInterwikiLink(key, value);
    }

    @Override
    public void appendInternalLink(String topic, String hashSection, String topicDescription, String cssClass,
            boolean parseRecursive) {

        //TODO: The MediaWikiScheme should be implemented here as configurable parameter see Bug 4627
        topic = topic.replace(" ", "_");

        WPATag aTagNode = new WPATag();
        // append(aTagNode);
        aTagNode.addAttribute("title", topic, true);

        String href = EndpointImpl.api().getRequestMapper()
                .getRequestStringFromValue(EndpointImpl.api().getNamespaceService().guessURI(topic));
        // in some cases it might not be possible to resolve user-defined
        // links; in case resolving fails, we have to make sure to avoid an NPE
        if (href == null) {
            logger.debug("Link for '" + topic + "' could not be resolved");
            href = "";
        }

        if (hashSection != null) {
            href = href + '#' + encodeTitleDotUrl(hashSection, true);
        }
        aTagNode.addAttribute("href", href, true);
        if (cssClass != null) {
            aTagNode.addAttribute("class", cssClass, true);
        }
        aTagNode.addObjectAttribute("wikilink", topic);

        pushNode(aTagNode);
        if (parseRecursive) {
            WikipediaParser.parseRecursive(topicDescription.trim(), this, false, true);
        } else {
            aTagNode.addChild(new ContentToken(StringEscapeUtils.escapeHtml(topicDescription)));
        }
        popNode();
    }

    private void appendErrorMessage(String msg) {
        TagNode div = new TagNode("div");
        pushNode(div);

        pushNode(new TagNode("br"));
        popNode();

        pushNode(new TagNode("pre"));
        append(new ContentToken(msg));
        popNode();
        popNode();
    }

    /**
     * @return the imageLinks
     */
    public Set<String> getImageLinks() {
        return imageLinks;
    }

    /**
     * Resolves the included template:
     * 
     * replaces all occurrences of named template parameters within 
     * the template by it's parsed value
     * 
     * 
     * @param templ
     * @param templateName
     * @param templateParameters
     * @return
     */
    public String resolveIncludedTemplate(String templ, String templateName,
            Map<String, String> templateParameters) {
        // Check for infinite loops is done in fluidwikimodel

        // First replace all template arguments with their parsed value(s)
        String res = templ;
        for (Map.Entry<String, String> entry : templateParameters.entrySet()) {
            String argName = "{{{" + entry.getKey() + "}}}";
            String value = parseTrim(entry.getValue(), this);
            res = res.replace(argName, value);
        }
        return res;
    }

    /**
     * Looks up the template for the given arguments in the file system
     * and returns the content (if available)
     * 
     * If the current user does not have read access to the template
     * (checked with {@link UserManager#hasValueAccess(Value, ValueAccessLevel)}),
     * the empty string is returned, i.e. the user cannot see the content.
     * 
     * @param templateName
     * @param namespace
     * @param versionFinal
     * @return the template content or null if the template page does not exist
     * 
     * @throws IllegalArgumentException if the templateName cannot be resolved to a valid URI
     */
    public String getIncludedTemplate(String templateName, String namespace, Date versionFinal)
            throws IllegalArgumentException {
        URI templateURI = resolveTemplateURI(templateName, namespace);

        // check the URI is valid
        if (templateURI == null)
            throw new IllegalArgumentException(
                    "Cannot resolve template '" + templateName.replaceAll(" ", "_") + "' as include (invalid URI)");

        // forbid include of templates to which user has no access
        if (!EndpointImpl.api().getUserManager().hasValueAccess(templateURI, ValueAccessLevel.READ))
            return ""; // do not render anything in case the user does not have access rights to the include

        String templ = Wikimedia.getWikiContent(templateURI, versionFinal);

        return templ;
    }

    /**
     * Returns a URI object for the given templateName adding the 'Template:' namespace where necessary. 
     * 
     * @param templateName
     * @param namespace
     * @return template URI if templateName was resolved, null if the template page does not exist.  
     */
    public static URI resolveTemplateURI(String templateName, String namespace) {

        templateName = templateName.replaceAll(" ", "_");
        NamespaceService ns = EndpointImpl.api().getNamespaceService();
        ValueFactory vf = ValueFactoryImpl.getInstance();

        URI templateURI;
        /* we try to place the URI in the correct namespace
          *                 
         1) :Person              -> default:Person
         2) Person               -> Template:default:Person
         3) Template:Person      -> Template:default:Person
         4) Template:foaf:Person -> Template:foaf:Person
         5) foaf:Person          -> foaf:Person
         */

        if ("".equals(namespace)) //case 1
        {
            templateURI = ns.guessURI(templateName);
        } else //cases 2-5
        {
            if (!templateName.contains(":")) // case 2
            {
                URI uri = ns.guessURI(templateName);
                if (uri == null)
                    return null;

                // template name directly places in template namespace
                templateURI = vf.createURI(ns.templateNamespace() + uri.stringValue());
            } else // case 3-5
            {

                if (templateName.startsWith(ns.templateNamespace())) // case 3,4
                {
                    String name = templateName.substring(ns.templateNamespace().length());
                    URI uri = ns.guessURI(name);
                    if (uri == null)
                        return null;
                    templateURI = vf.createURI(ns.templateNamespace() + uri.stringValue());
                } else // case 5
                    templateURI = ns.guessURI(templateName);
            }
        }

        return templateURI;
    }
}