Package org.apache.shindig.gadgets.render

Source Code of org.apache.shindig.gadgets.render.RenderingGadgetRewriter

/*
* 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.render;

import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.JsonSerializer;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.common.xml.DomUtil;
import org.apache.shindig.config.ContainerConfig;
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.UnsupportedFeatureException;
import org.apache.shindig.gadgets.UrlGenerator;
import org.apache.shindig.gadgets.features.FeatureRegistry;
import org.apache.shindig.gadgets.features.FeatureResource;
import org.apache.shindig.gadgets.preload.PreloadException;
import org.apache.shindig.gadgets.preload.PreloadedData;
import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
import org.apache.shindig.gadgets.rewrite.MutableContent;
import org.apache.shindig.gadgets.rewrite.RewritingException;
import org.apache.shindig.gadgets.spec.Feature;
import org.apache.shindig.gadgets.spec.MessageBundle;
import org.apache.shindig.gadgets.spec.ModulePrefs;
import org.apache.shindig.gadgets.spec.UserPref;
import org.apache.shindig.gadgets.spec.View;
import org.apache.commons.lang.StringUtils;
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 java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
* Produces a valid HTML document for the gadget output, automatically inserting appropriate HTML
* document wrapper data as needed.
*
* Currently, this is only invoked directly since the rewriting infrastructure doesn't properly
* deal with uncacheable rewrite operations.
*
* TODO: Break this up into multiple rewriters.
*
* Should be:
*
* - UserPrefs injection
* - Javascript injection (including configuration)
* - html document normalization
*/
public class RenderingGadgetRewriter implements GadgetRewriter {
  private static final Logger LOG = Logger.getLogger(RenderingGadgetRewriter.class.getName());
 
  private static final int INLINE_JS_BUFFER = 50;

  protected static final String DEFAULT_CSS =
      "body,td,div,span,p{font-family:arial,sans-serif;}" +
      "a {color:#0000cc;}a:visited {color:#551a8b;}" +
      "a:active {color:#ff0000;}" +
      "body{margin: 0px;padding: 0px;background-color:white;}";
  static final String IS_GADGET_BEACON = "window['__isgadget']=true;";
  static final String INSERT_BASE_ELEMENT_KEY = "gadgets.insertBaseElement";
  static final String FEATURES_KEY = "gadgets.features";

  protected final MessageBundleFactory messageBundleFactory;
  protected final ContainerConfig containerConfig;
  protected final FeatureRegistry featureRegistry;
  protected final UrlGenerator urlGenerator;
  protected final RpcServiceLookup rpcServiceLookup;
  protected Set<String> defaultExternLibs = ImmutableSet.of();

  protected Boolean externalizeFeatures = false;

  /**
   * @param messageBundleFactory Used for injecting message bundles into gadget output.
   */
  @Inject
  public RenderingGadgetRewriter(MessageBundleFactory messageBundleFactory,
                                 ContainerConfig containerConfig,
                                 FeatureRegistry featureRegistry,
                                 UrlGenerator urlGenerator,
                                 RpcServiceLookup rpcServiceLookup) {
    this.messageBundleFactory = messageBundleFactory;
    this.containerConfig = containerConfig;
    this.featureRegistry = featureRegistry;
    this.urlGenerator = urlGenerator;
    this.rpcServiceLookup = rpcServiceLookup;
  }

  @Inject
  public void setDefaultForcedLibs(@Named("shindig.gadget-rewrite.default-forced-libs")String forcedLibs) {
    if (StringUtils.isNotBlank(forcedLibs)) {
      defaultExternLibs = ImmutableSortedSet.of(StringUtils.split(forcedLibs, ':'));
    }
  }

  @Inject(optional = true)
  public void setExternalizeFeatureLibs(@Named("shindig.gadget-rewrite.externalize-feature-libs")Boolean externalizeFeatures) {
    this.externalizeFeatures = externalizeFeatures;
  }

  public void rewrite(Gadget gadget, MutableContent mutableContent) throws RewritingException {
    // Don't touch sanitized gadgets.
    if (gadget.sanitizeOutput()) {
      return;
    }

    try {
      Document document = mutableContent.getDocument();

      Element head = (Element)DomUtil.getFirstNamedChildNode(document.getDocumentElement(), "head");

      // Remove all the elements currently in head and add them back after we inject content
      NodeList children = head.getChildNodes();
      List<Node> existingHeadContent = Lists.newArrayListWithExpectedSize(children.getLength());
      for (int i = 0; i < children.getLength(); i++) {
        existingHeadContent.add(children.item(i));
      }

      for (Node n : existingHeadContent) {
        head.removeChild(n);
      }

      // Only inject default styles if no doctype was specified.
      if (document.getDoctype() == null) {
        Element defaultStyle = document.createElement("style");
        defaultStyle.setAttribute("type", "text/css");
        head.appendChild(defaultStyle);
        defaultStyle.appendChild(defaultStyle.getOwnerDocument().
            createTextNode(DEFAULT_CSS));
      }

      injectBaseTag(gadget, head);
      injectGadgetBeacon(gadget, head);
      injectFeatureLibraries(gadget, head);

      // This can be one script block.
      Element mainScriptTag = document.createElement("script");
      GadgetContext context = gadget.getContext();
      MessageBundle bundle = messageBundleFactory.getBundle(
          gadget.getSpec(), context.getLocale(), context.getIgnoreCache());
      injectMessageBundles(bundle, mainScriptTag);
      injectDefaultPrefs(gadget, mainScriptTag);
      injectPreloads(gadget, mainScriptTag);

      // We need to inject our script before any developer scripts.
      head.appendChild(mainScriptTag);

      Element body = (Element)DomUtil.getFirstNamedChildNode(document.getDocumentElement(), "body");

      body.setAttribute("dir", bundle.getLanguageDirection());

      // re append head content
      for (Node node : existingHeadContent) {
        head.appendChild(node);
      }

      injectOnLoadHandlers(body);

      mutableContent.documentChanged();
    } catch (GadgetException e) {
      throw new RewritingException(e.getLocalizedMessage(), e, e.getHttpStatusCode());
    }
  }

  protected void injectBaseTag(Gadget gadget, Node headTag) {
    GadgetContext context = gadget.getContext();
    if (containerConfig.getBool(context.getContainer(), INSERT_BASE_ELEMENT_KEY)) {
      Uri base = gadget.getSpec().getUrl();
      View view = gadget.getCurrentView();
      if (view != null && view.getHref() != null) {
        base = view.getHref();
      }
      Element baseTag = headTag.getOwnerDocument().createElement("base");
      baseTag.setAttribute("href", base.toString());
      headTag.insertBefore(baseTag, headTag.getFirstChild());
    }
  }

  protected void injectOnLoadHandlers(Node bodyTag) {
    Element onloadScript = bodyTag.getOwnerDocument().createElement("script");
    bodyTag.appendChild(onloadScript);
    onloadScript.appendChild(bodyTag.getOwnerDocument().createTextNode(
        "gadgets.util.runOnLoadHandlers();"));
  }
 
  protected void injectGadgetBeacon(Gadget gadget, Node headTag) throws GadgetException {
    Element beaconNode = headTag.getOwnerDocument().createElement("script");
    beaconNode.setTextContent(IS_GADGET_BEACON);
    headTag.appendChild(beaconNode);
  }

  /**
   * Injects javascript libraries needed to satisfy feature dependencies.
   */
  protected void injectFeatureLibraries(Gadget gadget, Node headTag) throws GadgetException {
    // TODO: If there isn't any js in the document, we can skip this. Unfortunately, that means
    // both script tags (easy to detect) and event handlers (much more complex).
    GadgetContext context = gadget.getContext();

    // Set of extern libraries requested by the container
    Set<String> externForcedLibs = defaultExternLibs;

    // gather the libraries we'll need to generate the extern libs
    String externParam = context.getParameter("libs");   
    if (StringUtils.isNotBlank(externParam)) {
      externForcedLibs = Sets.newTreeSet(Arrays.asList(StringUtils.split(externParam, ':')));
    }

    if (!externForcedLibs.isEmpty()) {
      String jsUrl = urlGenerator.getBundledJsUrl(externForcedLibs, context);
      Element libsTag = headTag.getOwnerDocument().createElement("script");
      libsTag.setAttribute("src", jsUrl);
      headTag.appendChild(libsTag);
    }

    List<String> unsupported = Lists.newLinkedList();

    List<FeatureResource> externForcedResources =
        featureRegistry.getFeatureResources(context, externForcedLibs, unsupported);
    if (!unsupported.isEmpty()) {
      LOG.info("Unknown feature(s) in extern &libs=: " + unsupported.toString());
      unsupported.clear();
    }

    // Get all resources requested by the gadget's requires/optional features.
    Map<String, Feature> featureMap = gadget.getSpec().getModulePrefs().getFeatures();
    List<String> gadgetFeatureKeys = Lists.newArrayList(gadget.getDirectFeatureDeps());
    List<FeatureResource> gadgetResources =
        featureRegistry.getFeatureResources(context, gadgetFeatureKeys, unsupported);
    if (!unsupported.isEmpty()) {
      List<String> requiredUnsupported = Lists.newLinkedList();
      for (String notThere : unsupported) {
        if (!featureMap.containsKey(notThere) || featureMap.get(notThere).getRequired()) {
          // if !containsKey, the lib was forced with Gadget.addFeature(...) so implicitly req'd.
          requiredUnsupported.add(notThere);
        }
      }
      if (!requiredUnsupported.isEmpty()) {
        throw new UnsupportedFeatureException(requiredUnsupported.toString());
      }
    }   

    // Inline or externalize the gadgetFeatureKeys
    List<FeatureResource> inlineResources = Lists.newArrayList();       
    List<String> allRequested = Lists.newArrayList(gadgetFeatureKeys);

    if (externalizeFeatures) {
      Set<String> externGadgetLibs = Sets.newTreeSet(featureRegistry.getFeatures(gadgetFeatureKeys));
      externGadgetLibs.removeAll(externForcedLibs);

      if (!externGadgetLibs.isEmpty()) {
        String jsUrl = urlGenerator.getBundledJsUrl(externGadgetLibs, context);
        Element libsTag = headTag.getOwnerDocument().createElement("script");
        libsTag.setAttribute("src", jsUrl);
        headTag.appendChild(libsTag);
      }
    } else {
      inlineResources.addAll(gadgetResources);
    }

    // Calculate inlineResources as all resources that are needed by the gadget to
    // render, minus all those included through externResources.
    // TODO: profile and if needed, optimize this a bit.
    if (!externForcedLibs.isEmpty()) {
      allRequested.addAll(externForcedLibs);
      inlineResources.removeAll(externForcedResources);
    }

    // Precalculate the maximum length in order to avoid excessive garbage generation.
    int size = 0;
    for (FeatureResource resource : inlineResources) {
      if (!resource.isExternal()) {
        if (context.getDebug()) {
          size += resource.getDebugContent().length();
        } else {
          size += resource.getContent().length();
        }
      }
    }

    String libraryConfig =
        getLibraryConfig(gadget, featureRegistry.getFeatures(allRequested));
   
    // Size has a small fudge factor added to it for delimiters and such.
    StringBuilder inlineJs = new StringBuilder(size + libraryConfig.length() + INLINE_JS_BUFFER);

    // Inline any libs that weren't extern. The ugly context switch between inline and external
    // Js is needed to allow both inline and external scripts declared in feature.xml.
    for (FeatureResource resource : inlineResources) {
      String theContent = context.getDebug() ? resource.getDebugContent() : resource.getContent();
      if (resource.isExternal()) {
        if (inlineJs.length() > 0) {
          Element inlineTag = headTag.getOwnerDocument().createElement("script");
          headTag.appendChild(inlineTag);
          inlineTag.appendChild(headTag.getOwnerDocument().createTextNode(inlineJs.toString()));
          inlineJs.setLength(0);
        }
        Element referenceTag = headTag.getOwnerDocument().createElement("script");
        referenceTag.setAttribute("src", theContent);
        headTag.appendChild(referenceTag);
      } else {
        inlineJs.append(theContent).append(";\n");
      }
    }

    inlineJs.append(libraryConfig);

    if (inlineJs.length() > 0) {
      Element inlineTag = headTag.getOwnerDocument().createElement("script");
      headTag.appendChild(inlineTag);
      inlineTag.appendChild(headTag.getOwnerDocument().createTextNode(inlineJs.toString()));
    }
  }

  /**
   * Creates a set of all configuration needed to satisfy the requested feature set.
   *
   * Appends special configuration for gadgets.util.hasFeature and gadgets.util.getFeatureParams to
   * the output js.
   *
   * This can't be handled via the normal configuration mechanism because it is something that
   * varies per request.
   *
   * @param reqs The features needed to satisfy the request.
   * @throws GadgetException If there is a problem with the gadget auth token
   */
  protected String getLibraryConfig(Gadget gadget, List<String> reqs)
      throws GadgetException {
    GadgetContext context = gadget.getContext();

    Map<String, Object> features = containerConfig.getMap(context.getContainer(), FEATURES_KEY);

    Map<String, Object> config
        = Maps.newHashMapWithExpectedSize(features == null ? 2 : features.size() + 2);

    if (features != null) {
      // Discard what we don't care about.
      for (String name : reqs) {
        Object conf = features.get(name);
        if (conf != null) {
          config.put(name, conf);
        }
      }
    }

    addHasFeatureConfig(gadget, config);
    addOsapiSystemListMethodsConfig(context.getContainer(), config, context.getHost());
    addSecurityTokenConfig(context, config);
    return "gadgets.config.init(" + JsonSerializer.serialize(config) + ");\n";
  }

  private void addSecurityTokenConfig(GadgetContext context, Map<String, Object> config) {
    SecurityToken authToken = context.getToken();
    if (authToken != null) {
      Map<String, String> authConfig = Maps.newHashMapWithExpectedSize(2);
      String updatedToken = authToken.getUpdatedToken();
      if (updatedToken != null) {
        authConfig.put("authToken", updatedToken);
      }
      String trustedJson = authToken.getTrustedJson();
      if (trustedJson != null) {
        authConfig.put("trustedJson", trustedJson);
      }
      config.put("shindig.auth", authConfig);
    }
  }

  private void addHasFeatureConfig(Gadget gadget, Map<String, Object> config) {
    // Add gadgets.util support. This is calculated dynamically based on request inputs.
    ModulePrefs prefs = gadget.getSpec().getModulePrefs();
    Collection<Feature> features = prefs.getFeatures().values();
    Map<String, Map<String, Object>> featureMap = Maps.newHashMapWithExpectedSize(features.size());
    for (Feature feature : features) {
     
      // Flatten out the multimap a bit for backwards compatibility:  map keys
      // with just 1 value into the string, treat others as arrays
      Map<String, Object> paramFeaturesInConfig = Maps.newHashMap();
      for (String paramName : feature.getParams().keySet()) {
        Collection<String> paramValues = feature.getParams().get(paramName);
        if (paramValues.size() == 1) {
          paramFeaturesInConfig.put(paramName, paramValues.iterator().next());
        } else {
          paramFeaturesInConfig.put(paramName, paramValues);
        }
      }
     
      featureMap.put(feature.getName(), paramFeaturesInConfig);
    }
    config.put("core.util", featureMap);
  }

  private void addOsapiSystemListMethodsConfig(String container, Map<String, Object> config,
      String host) {
    if (rpcServiceLookup != null) {
      Multimap<String, String> endpoints = rpcServiceLookup.getServicesFor(container, host);
      config.put("osapi.services", endpoints);
    }
  }

  /**
   * Injects message bundles into the gadget output.
   * @throws GadgetException If we are unable to retrieve the message bundle.
   */
  protected void injectMessageBundles(MessageBundle bundle, Node scriptTag) throws GadgetException {
    String msgs = bundle.toJSONString();

    Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.Prefs.setMessages_(");
    text.appendData(msgs);
    text.appendData(");");
    scriptTag.appendChild(text);
  }

  /**
   * Injects default values for user prefs into the gadget output.
   */
  protected void injectDefaultPrefs(Gadget gadget, Node scriptTag) {
    List<UserPref> prefs = gadget.getSpec().getUserPrefs();
    Map<String, String> defaultPrefs = Maps.newHashMapWithExpectedSize(prefs.size());
    for (UserPref up : prefs) {
      defaultPrefs.put(up.getName(), up.getDefaultValue());
    }
    Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.Prefs.setDefaultPrefs_(");
    text.appendData(JsonSerializer.serialize(defaultPrefs));
    text.appendData(");");
    scriptTag.appendChild(text);
  }

  /**
   * Injects preloads into the gadget output.
   *
   * If preloading fails for any reason, we just output an empty object.
   */
  protected void injectPreloads(Gadget gadget, Node scriptTag) {
    List<Object> preload = Lists.newArrayList();
    for (PreloadedData preloaded : gadget.getPreloads()) {
      try {
        preload.addAll(preloaded.toJson());
      } catch (PreloadException pe) {
        // This will be thrown in the event of some unexpected exception. We can move on.
        LOG.log(Level.WARNING, "Unexpected error when preloading", pe);
      }
    }
    Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.io.preloaded_=");
    text.appendData(JsonSerializer.serialize(preload));
    text.appendData(";");
    scriptTag.appendChild(text);
  }
}
TOP

Related Classes of org.apache.shindig.gadgets.render.RenderingGadgetRewriter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.