org.apache.shindig.gadgets.rewrite.TemplateRewriter.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.shindig.gadgets.rewrite.TemplateRewriter.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.shindig.gadgets.rewrite;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.apache.commons.lang.StringUtils;
import org.apache.shindig.common.JsonSerializer;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.common.xml.DomUtil;
import org.apache.shindig.expressions.Expressions;
import org.apache.shindig.gadgets.Gadget;
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.MessageBundleFactory;
import org.apache.shindig.gadgets.parse.SocialDataTags;
import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
import org.apache.shindig.gadgets.spec.Feature;
import org.apache.shindig.gadgets.spec.MessageBundle;
import org.apache.shindig.gadgets.templates.ContainerTagLibraryFactory;
import org.apache.shindig.gadgets.templates.MessageELResolver;
import org.apache.shindig.gadgets.templates.TagRegistry;
import org.apache.shindig.gadgets.templates.TemplateContext;
import org.apache.shindig.gadgets.templates.TemplateLibrary;
import org.apache.shindig.gadgets.templates.TemplateLibraryFactory;
import org.apache.shindig.gadgets.templates.TemplateParserException;
import org.apache.shindig.gadgets.templates.TemplateProcessor;
import org.apache.shindig.gadgets.templates.TemplateResource;
import org.apache.shindig.gadgets.templates.tags.CompositeTagRegistry;
import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
import org.apache.shindig.gadgets.templates.tags.TagHandler;
import org.apache.shindig.gadgets.templates.tags.TemplateBasedTagHandler;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This ContentRewriter uses a TemplateProcessor to replace os-template
 * tag contents of a gadget spec with their rendered equivalents.
 *
 * Only templates without the @name and @tag attributes are processed
 * automatically.
 */
public class TemplateRewriter implements GadgetRewriter {

    public final static Set<String> TAGS = ImmutableSet.of("script");
    public static final String TEMPLATES_FEATURE_NAME = "opensocial-templates";
    public static final String OSML_FEATURE_NAME = "osml";

    /** Set to true to block auto-processing of templates */
    static final String DISABLE_AUTO_PROCESSING_PARAM = "disableAutoProcessing";

    /** Specifies what template libraries to load */
    static final String REQUIRE_LIBRARY_PARAM = "requireLibrary";

    /** Enable client support? **/
    static final String CLIENT_SUPPORT_PARAM = "client";

    private static final Logger LOG = Logger.getLogger(TemplateRewriter.class.getName());

    /**
     * Provider of the processor.  TemplateRewriters are stateless and multithreaded,
     * processors are not.
     */
    private final Provider<TemplateProcessor> processor;
    private final MessageBundleFactory messageBundleFactory;
    private final Expressions expressions;
    private final TagRegistry baseTagRegistry;
    private final TemplateLibraryFactory libraryFactory;
    private final ContainerTagLibraryFactory containerTags;

    @Inject
    public TemplateRewriter(Provider<TemplateProcessor> processor, MessageBundleFactory messageBundleFactory,
            Expressions expressions, TagRegistry baseTagRegistry, TemplateLibraryFactory libraryFactory,
            ContainerTagLibraryFactory containerTags) {
        this.processor = processor;
        this.messageBundleFactory = messageBundleFactory;
        this.expressions = expressions;
        this.baseTagRegistry = baseTagRegistry;
        this.libraryFactory = libraryFactory;
        this.containerTags = containerTags;
    }

    public void rewrite(Gadget gadget, MutableContent content) throws RewritingException {
        Map<String, Feature> directFeatures = gadget.getSpec().getModulePrefs().getFeatures();

        Feature feature = directFeatures.get(TEMPLATES_FEATURE_NAME);
        if (feature == null && directFeatures.containsKey(OSML_FEATURE_NAME)) {
            feature = directFeatures.get(OSML_FEATURE_NAME);
        }

        if (feature != null && isServerTemplatingEnabled(feature)) {
            try {
                rewriteImpl(gadget, feature, content);
            } catch (GadgetException ge) {
                throw new RewritingException(ge, ge.getHttpStatusCode());
            }
        }
    }

    /**
     * Disable server-side templating when the feature contains:
     * <pre>
     *   &lt;Param name="disableAutoProcessing"&gt;true&lt;/Param&gt;
     * </pre>
     */
    private boolean isServerTemplatingEnabled(Feature feature) {
        return (!"true".equalsIgnoreCase(feature.getParam(DISABLE_AUTO_PROCESSING_PARAM)));
    }

    private void rewriteImpl(Gadget gadget, Feature feature, MutableContent content) throws GadgetException {
        List<TagRegistry> registries = Lists.newArrayList();
        List<TemplateLibrary> libraries = Lists.newArrayList();

        // TODO: Add View-specific library as Priority 0

        // Built-in Java-based tags - Priority 1
        registries.add(baseTagRegistry);

        TemplateLibrary osmlLibrary = containerTags.getLibrary(gadget.getContext().getContainer());

        // OSML Built-in tags - Priority 2
        registries.add(osmlLibrary.getTagRegistry());
        libraries.add(osmlLibrary);

        List<Element> templateElements = SocialDataTags.getTags(content.getDocument(),
                SocialDataTags.OSML_TEMPLATE_TAG);
        List<Element> templates = ImmutableList.copyOf(templateElements);

        if (!OSML_FEATURE_NAME.equals(feature.getName())) {
            // User-defined custom tags - Priority 3
            registries.add(registerCustomTags(templates));

            // User-defined libraries - Priority 4
            loadTemplateLibraries(gadget.getContext(), feature, registries, libraries);
        }

        TagRegistry registry = new CompositeTagRegistry(registries);

        TemplateContext templateContext = new TemplateContext(gadget, content.getPipelinedData());
        boolean needsFeature = executeTemplates(templateContext, content, templates, registry);

        // Check if a feature param overrides  our guess at whether the client-side    
        // feature is needed.                                                  
        String clientOverride = feature.getParam(CLIENT_SUPPORT_PARAM);
        if ("true".equalsIgnoreCase(clientOverride)) {
            needsFeature = true;
        } else if ("false".equalsIgnoreCase(clientOverride)) {
            needsFeature = false;
        }

        Element head = (Element) DomUtil.getFirstNamedChildNode(content.getDocument().getDocumentElement(), "head");
        postProcess(templateContext, needsFeature, head, templates, libraries);
    }

    /**
     * Post-processes the gadget content after rendering templates.
     * 
     * @param templateContext TemplateContext to operate on
     * @param needsFeature Should the templates feature be made available to 
     * client?
     * @param head Head element of the gadget's document
     * @param libraries Keeps track of all libraries, and which got used
     * @param allTemplates A list of all the template nodes
     * @param libraries A list of all registered libraries
     */
    private void postProcess(TemplateContext templateContext, boolean needsFeature, Element head,
            List<Element> allTemplates, List<TemplateLibrary> libraries) {
        // Inject all the needed library assets.
        // TODO: inject library assets that aren't used on the server, but will
        // be needed on the client
        for (TemplateResource resource : templateContext.getResources()) {
            injectTemplateLibraryAssets(resource, head);
        }

        // If we don't need the feature, remove it and all templates from the gadget
        if (!needsFeature) {
            templateContext.getGadget().removeFeature(TEMPLATES_FEATURE_NAME);
            for (Element template : allTemplates) {
                Node parent = template.getParentNode();
                if (parent != null) {
                    parent.removeChild(template);
                }
            }
        } else {
            // If the feature is to be kept, inject the libraries.
            // Library assets will be generated on the client.
            // TODO: only inject the templates, not the full scripts/styles
            for (TemplateLibrary library : libraries) {
                injectTemplateLibrary(library, head);
            }
        }
    }

    private void loadTemplateLibraries(GadgetContext context, Feature feature, List<TagRegistry> registries,
            List<TemplateLibrary> libraries) throws GadgetException {
        Collection<String> urls = feature.getParams().get(REQUIRE_LIBRARY_PARAM);
        if (urls != null) {
            for (String url : urls) {
                Uri uri = Uri.parse(url.trim());
                uri = context.getUrl().resolve(uri);

                try {
                    TemplateLibrary library = libraryFactory.loadTemplateLibrary(context, uri);
                    registries.add(library.getTagRegistry());
                    libraries.add(library);
                } catch (TemplateParserException te) {
                    // Suppress exceptions due to malformed template libraries
                    LOG.log(Level.WARNING, null, te);
                }
            }
        }
    }

    private void injectTemplateLibraryAssets(TemplateResource resource, Element head) {
        Element contentElement;
        switch (resource.getType()) {
        case JAVASCRIPT:
            contentElement = head.getOwnerDocument().createElement("script");
            contentElement.setAttribute("type", "text/javascript");
            break;
        case STYLE:
            contentElement = head.getOwnerDocument().createElement("style");
            contentElement.setAttribute("type", "text/css");
            break;
        default:
            throw new IllegalStateException("Unhandled type");
        }

        if (resource.isSafe()) {
            SanitizingGadgetRewriter.bypassSanitization(contentElement, false);
        }
        contentElement.setTextContent(resource.getContent());
        head.appendChild(contentElement);
    }

    private void injectTemplateLibrary(TemplateLibrary library, Element head) {
        try {
            String libraryContent = library.serialize();
            if (StringUtils.isEmpty(libraryContent)) {
                return;
            }

            Element scriptElement = head.getOwnerDocument().createElement("script");
            scriptElement.setAttribute("type", "text/javascript");
            StringBuilder buffer = new StringBuilder();
            buffer.append("opensocial.template.Loader.loadContent(");
            JsonSerializer.appendString(buffer, library.serialize());
            buffer.append(',');
            JsonSerializer.appendString(buffer, library.getLibraryUri().toString());
            buffer.append(");");
            scriptElement.setTextContent(buffer.toString());
            head.appendChild(scriptElement);
        } catch (IOException ioe) {
            // This should never happen.
        }
    }

    /**
     * Register templates with a "tag" attribute.
     */
    private TagRegistry registerCustomTags(List<Element> allTemplates) {
        ImmutableSet.Builder<TagHandler> handlers = ImmutableSet.builder();
        for (Element template : allTemplates) {
            // Only process templates with a tag attribute
            if (template.getAttribute("tag").length() == 0) {
                continue;
            }

            String[] nameParts = StringUtils.splitPreserveAllTokens(template.getAttribute("tag"), ':');
            // At this time, we only support 
            if (nameParts.length != 2) {
                continue;
            }
            String namespaceUri = template.lookupNamespaceURI(nameParts[0]);
            if (namespaceUri != null) {
                handlers.add(new TemplateBasedTagHandler(template, namespaceUri, nameParts[1]));
            }
        }

        return new DefaultTagRegistry(handlers.build());
    }

    /**
     * Processes and renders inline templates.
     * @return Do we think the templates feature is still needed on the client?
     */
    private boolean executeTemplates(TemplateContext templateContext, MutableContent content,
            List<Element> allTemplates, TagRegistry registry) throws GadgetException {
        Map<String, Object> pipelinedData = content.getPipelinedData();

        // If true, client-side processing will be needed
        boolean needsFeature = false;
        List<Element> templates = Lists.newArrayList();
        for (Element element : allTemplates) {
            String tag = element.getAttribute("tag");
            String require = element.getAttribute("require");

            if (!checkRequiredData(require, pipelinedData.keySet())) {
                // Can't be processed on the server at all;  keep client-side processing
                needsFeature = true;
            } else if ("".equals(tag)) {
                templates.add(element);
            }
        }

        if (!templates.isEmpty()) {
            Gadget gadget = templateContext.getGadget();

            MessageBundle bundle = messageBundleFactory.getBundle(gadget.getSpec(), gadget.getContext().getLocale(),
                    gadget.getContext().getIgnoreCache(), gadget.getContext().getContainer());
            MessageELResolver messageELResolver = new MessageELResolver(expressions, bundle);

            int autoUpdateID = 0;
            for (Element template : templates) {
                DocumentFragment result = processor.get().processTemplate(template, templateContext,
                        messageELResolver, registry);
                // TODO: sanitized renders should ignore this value
                if ("true".equals(template.getAttribute("autoUpdate"))) {
                    // autoUpdate requires client-side processing.
                    needsFeature = true;
                    Element span = template.getOwnerDocument().createElement("span");
                    String id = "template_auto" + (autoUpdateID++);
                    span.setAttribute("id", "_T_" + id);
                    template.setAttribute("name", id);
                    template.getParentNode().insertBefore(span, template);
                    span.appendChild(result);
                } else {
                    template.getParentNode().insertBefore(result, template);
                    template.getParentNode().removeChild(template);
                }
            }
            MutableContent.notifyEdit(content.getDocument());
        }
        return needsFeature;
    }

    /**
     * Checks that all the required data is available at rewriting time.
     * @param requiredData A string of comma-separated data set names
     * @param availableData A map of available data sets
     * @return true if all required data sets are present, false otherwise
     */
    private static boolean checkRequiredData(String requiredData, Set<String> availableData) {
        if ("".equals(requiredData)) {
            return true;
        }
        StringTokenizer st = new StringTokenizer(requiredData, ",");
        while (st.hasMoreTokens()) {
            if (!availableData.contains(st.nextToken().trim())) {
                return false;
            }
        }
        return true;
    }
}