/* * 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 org.apache.shindig.common.ContainerConfig; 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.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; /** * 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"); /** * 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 : StringUtils.split(ResourceLoader.getContent(location), "[\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 { 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) { logger.info("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 = JsLibrary.create( type, content, feature.name, inlineOk ? fetcher : null); for (String cont : container.split(",")) { feature.addLibrary(renderingContext, cont.trim(), library); } } } /** * @param fetcher */ public JsFeatureLoader(HttpFetcher fetcher) { this.fetcher = 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); } }