org.apache.shindig.gadgets.JsFeatureLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.shindig.gadgets.JsFeatureLoader.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;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;

import org.apache.shindig.common.util.ResourceLoader;
import org.apache.shindig.common.xml.XmlException;
import org.apache.shindig.common.xml.XmlUtil;
import org.apache.shindig.config.ContainerConfig;
import org.apache.shindig.gadgets.http.HttpFetcher;

import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.io.File;
import java.io.IOException;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Logger;
import java.util.logging.Level;

/**
 * Provides a mechanism for loading a group of js features from a directory.
 *
 * All directories from the given input will be checked recursively for files
 * named "feature.xml"
 *
 * Usage:
 * GadgetFeatureRegistry registry = // get your feature registry.
 * JsFeatureLoader loader = new JsFeatureLoader(fetcher);
 * loader.loadFeatures("res://features/", registry);
 * loader.loadFeatures("/home/user/my-features/", registry);
 */
public class JsFeatureLoader {

    public final static char FILE_SEPARATOR = ',';

    private final HttpFetcher fetcher;

    private static final Logger logger = Logger.getLogger("org.apache.shindig.gadgets");

    /**
     * @param fetcher
     */
    @Inject
    public JsFeatureLoader(HttpFetcher fetcher) {
        this.fetcher = fetcher;
    }

    /**
     * Loads all of the gadgets in the directory specified by path. Invalid
     * features will not cause this to fail, but passing an invalid path will.
     *
     * @param path The file or directory to load the feature from. If feature.xml
     *    is passed in directly, it will be loaded as a single feature. If a
     *    directory is passed, any features in that directory (recursively) will
     *    be loaded. If res://*.txt is passed, we will look for named resources
     *    in the text file. If path is prefixed with res://, the file
     *    is treated as a resource, and all references are assumed to be
     *    resources as well. Multiple locations may be specified by separating
     *    them with a comma.
     * @throws GadgetException If any of the files can't be read.
     */
    public void loadFeatures(String path, GadgetFeatureRegistry registry) throws GadgetException {
        List<ParsedFeature> features = Lists.newLinkedList();
        try {
            for (String location : StringUtils.split(path, FILE_SEPARATOR)) {
                if (location.startsWith("res://")) {
                    location = location.substring(6);
                    logger.info("Loading resources from: " + location);
                    if (location.endsWith(".txt")) {
                        List<String> resources = Lists.newArrayList();
                        for (String resource : ResourceLoader.getContent(location).split("[\r\n]+")) {
                            // Skip blank/commented lines
                            if (StringUtils.trim(resource).length() > 0 && resource.charAt(0) != '#') {
                                resources.add(StringUtils.trim(resource));
                            }
                        }
                        loadResources(resources, features);
                    } else {
                        loadResources(ImmutableList.of(location), features);
                    }
                } else {
                    logger.info("Loading files from: " + location);
                    File file = new File(location);
                    loadFiles(new File[] { file }, features);
                }
            }
        } catch (IOException e) {
            throw new GadgetException(GadgetException.Code.INVALID_PATH, e);
        }

        for (ParsedFeature feature : features) {
            GadgetFeature gadgetFeature = new GadgetFeature(feature.name, feature.libraries, feature.deps);
            registry.register(gadgetFeature);
        }
    }

    /**
     * Parses and registers a single feature xml.
     * Used for testing.
     *
     * @param xml
     * @return The parsed feature.
     */
    public GadgetFeature loadFeature(GadgetFeatureRegistry registry, String xml) throws GadgetException {
        ParsedFeature parsed = parse(xml, "", false);
        GadgetFeature feature = new GadgetFeature(parsed.name, parsed.libraries, parsed.deps);
        registry.register(feature);
        return feature;
    }

    /**
     * Loads features from directories recursively.
     * @param files The files to examine.
     * @param features The set of all loaded features
     * @throws GadgetException
     */
    private void loadFiles(File[] files, List<ParsedFeature> features) throws GadgetException {
        for (File file : files) {
            if (file.isDirectory()) {
                loadFiles(file.listFiles(), features);
            } else if (file.getName().toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
                if (!file.exists()) {
                    throw new GadgetException(GadgetException.Code.INVALID_PATH,
                            "The file '" + file.getAbsolutePath() + "' doesn't exist.");
                }
                ParsedFeature feature = processFile(file);
                if (feature != null) {
                    features.add(feature);
                }
            } else {
                if (logger.isLoggable(Level.FINEST))
                    logger.finest(file.getAbsolutePath() + " doesn't seem to be an XML file.");
            }
        }
    }

    /**
     * Loads resources recursively.
     * @param paths The base paths to look for feature.xml
     * @param features The set of all loaded features
     * @throws GadgetException
     */
    private void loadResources(List<String> paths, List<ParsedFeature> features) throws GadgetException {
        try {
            for (String file : paths) {
                if (logger.isLoggable(Level.FINE))
                    logger.fine("Processing resource: " + file);
                String content = ResourceLoader.getContent(file);
                String parent = file.substring(0, file.lastIndexOf('/') + 1);
                ParsedFeature feature = parse(content, parent, true);
                if (feature != null) {
                    features.add(feature);
                } else {
                    logger.warning("Failed to parse feature: " + file);
                }
            }
        } catch (IOException e) {
            throw new GadgetException(GadgetException.Code.INVALID_PATH, e);
        }
    }

    /**
     * Loads a single feature from a file.
     *
     * If the file can't be loaded, an error will be generated but no exception
     * will be thrown.
     *
     * @param file The file that contains the feature description.
     * @return The parsed feature.
     */
    private ParsedFeature processFile(File file) {
        logger.info("Loading file: " + file.getName());
        ParsedFeature feature = null;
        if (file.canRead()) {
            try {
                feature = parse(ResourceLoader.getContent(file), file.getParent() + '/', false);
            } catch (IOException e) {
                logger.warning("Error reading file: " + file.getAbsolutePath());
            } catch (GadgetException e) {
                logger.warning("Failed parsing file: " + file.getAbsolutePath());
            }
        } else {
            logger.warning("Unable to read file: " + file.getAbsolutePath());
        }
        return feature;
    }

    /**
     * Parses the input into a dom tree.
     * @param xml
     * @param path The path the file was loaded from.
     * @param isResource True if the file was a resource.
     * @return A dom tree representing the feature.
     * @throws GadgetException
     */
    private ParsedFeature parse(String xml, String path, boolean isResource) throws GadgetException {
        Element doc;
        try {
            doc = XmlUtil.parse(xml);
        } catch (XmlException e) {
            throw new GadgetException(GadgetException.Code.MALFORMED_XML_DOCUMENT, e);
        }

        ParsedFeature feature = new ParsedFeature();

        feature.basePath = path;
        feature.isResource = isResource;

        NodeList nameNode = doc.getElementsByTagName("name");
        if (nameNode.getLength() != 1) {
            throw new GadgetException(GadgetException.Code.MALFORMED_XML_DOCUMENT, "No name provided");
        }
        feature.name = nameNode.item(0).getTextContent();

        NodeList gadgets = doc.getElementsByTagName("gadget");
        for (int i = 0, j = gadgets.getLength(); i < j; ++i) {
            processContext(feature, (Element) gadgets.item(i), RenderingContext.GADGET);
        }

        NodeList containers = doc.getElementsByTagName("container");
        for (int i = 0, j = containers.getLength(); i < j; ++i) {
            processContext(feature, (Element) containers.item(i), RenderingContext.CONTAINER);
        }

        NodeList dependencies = doc.getElementsByTagName("dependency");
        for (int i = 0, j = dependencies.getLength(); i < j; ++i) {
            feature.deps.add(dependencies.item(i).getTextContent());
        }

        return feature;
    }

    /**
     * Processes <gadget> and <container> tags and adds new libraries
     * to the feature.
     * @param feature
     * @param context
     * @param renderingContext
     * @throws GadgetException
     */
    private void processContext(ParsedFeature feature, Element context, RenderingContext renderingContext)
            throws GadgetException {
        String container = XmlUtil.getAttribute(context, "container", ContainerConfig.DEFAULT_CONTAINER);
        NodeList libraries = context.getElementsByTagName("script");
        for (int i = 0, j = libraries.getLength(); i < j; ++i) {
            Element script = (Element) libraries.item(i);
            boolean inlineOk = XmlUtil.getBoolAttribute(script, "inline", true);
            String source = XmlUtil.getAttribute(script, "src");
            String content;
            JsLibrary.Type type;
            if (source == null) {
                type = JsLibrary.Type.INLINE;
                content = script.getTextContent();
            } else {
                content = source;
                if (content.startsWith("http://")) {
                    type = JsLibrary.Type.URL;
                } else if (content.startsWith("//")) {
                    type = JsLibrary.Type.URL;
                    content = content.substring(1);
                } else if (content.startsWith("res://")) {
                    content = content.substring(6);
                    type = JsLibrary.Type.RESOURCE;
                } else if (feature.isResource) {
                    // Note: Any features loaded as resources will assume that their
                    // paths point to resources as well.
                    content = feature.basePath + content;
                    type = JsLibrary.Type.RESOURCE;
                } else {
                    content = feature.basePath + content;
                    type = JsLibrary.Type.FILE;
                }
            }
            JsLibrary library = createJsLibrary(type, content, feature.name, inlineOk ? fetcher : null);
            for (String cont : container.split(",")) {
                feature.addLibrary(renderingContext, cont.trim(), library);
            }
        }
    }

    protected JsLibrary createJsLibrary(JsLibrary.Type type, String content, String feature, HttpFetcher fetcher)
            throws GadgetException {
        return JsLibrary.create(type, content, feature, fetcher);
    }
}

/**
 * Temporary structure to represent the intermediary parse state.
 */
class ParsedFeature {
    public String name = "";
    public String basePath = "";
    public boolean isResource = false;
    final Map<RenderingContext, Map<String, List<JsLibrary>>> libraries;
    final List<String> deps;

    public ParsedFeature() {
        libraries = new EnumMap<RenderingContext, Map<String, List<JsLibrary>>>(RenderingContext.class);
        deps = Lists.newLinkedList();
    }

    public void addLibrary(RenderingContext ctx, String cont, JsLibrary library) {
        Map<String, List<JsLibrary>> ctxLibs = libraries.get(ctx);
        if (ctxLibs == null) {
            ctxLibs = Maps.newHashMap();
            libraries.put(ctx, ctxLibs);
        }

        List<JsLibrary> containerLibs = ctxLibs.get(cont);
        if (containerLibs == null) {
            containerLibs = Lists.newLinkedList();
            ctxLibs.put(cont, containerLibs);
        }

        containerLibs.add(library);
    }
}