Java tutorial
/* * 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 org.apache.commons.lang.StringUtils; import org.apache.shindig.common.uri.Uri; import org.apache.shindig.gadgets.Gadget; import org.apache.shindig.gadgets.uri.ConcatUriManager; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * DOM mutator that concatenates resources using the concat servlet * @since 2.0.0 */ public class ConcatVisitor implements DomWalker.Visitor { public static class Js extends ConcatVisitor { public Js(ContentRewriterFeature.Config config, ConcatUriManager uriManager) { super(config, uriManager, ConcatUriManager.Type.JS); } } public static class Css extends ConcatVisitor { public Css(ContentRewriterFeature.Config config, ConcatUriManager uriManager) { super(config, uriManager, ConcatUriManager.Type.CSS); } } private final ConcatUriManager uriManager; private final ConcatUriManager.Type type; private final ContentRewriterFeature.Config config; private final boolean split; private ConcatVisitor(ContentRewriterFeature.Config config, ConcatUriManager uriManager, ConcatUriManager.Type type) { this.uriManager = uriManager; this.type = type; this.config = config; this.split = (type == ConcatUriManager.Type.JS && config.isSplitJsEnabled()); } public VisitStatus visit(Gadget gadget, Node node) throws RewritingException { // Reserve JS nodes; always if there's an adjacent rewritable JS node and also when // directed to support split-resource concatenation if (node.getNodeType() != Node.ELEMENT_NODE || !node.getNodeName().equalsIgnoreCase(type.getTagName())) { return VisitStatus.BYPASS; } Element element = (Element) node; if (isRewritableExternData(element)) { if (split || isRewritableExternData(getSibling(element, true)) || isRewritableExternData(getSibling(element, false))) { return VisitStatus.RESERVE_NODE; } } return VisitStatus.BYPASS; } /** * For css: * Link tags are first split into buckets separated by tags with mediaType == "all" * / title attribute different from their previous link tag / nodes that are * not 'link' tags. * This ensures that the buckets can be processed separately without losing title / * "all" mediaType information. * * Link tags with same mediaType are concatenated within each bucket. * This exercise ensures that css information is loaded in the same relative order * as that of the original html page, and that the css information within * mediaType=="all" is retained and applies to all media types. * * Look at the areLinkNodesBucketable method for details on mediaType=="all" and * title attribute * * Example: Assume we have the following node list. (all have same parent, * nodes between Node6 and Node12 are non link nodes, and hence did not come * to revisit() call) * <link href="1.css" rel="stylesheet" type="text/css" media="screen"> -- Node1 * <link href="2.css" rel="stylesheet" type="text/css" media="print"> -- Node2 * <link href="3.css" rel="stylesheet" type="text/css" media="screen"> -- Node3 * <link href="4.css" rel="stylesheet" type="text/css" media="all"> -- Node4 * <link href="5.css" rel="stylesheet" type="text/css" media="all"> -- Node5 * <link href="6.css" rel="stylesheet" type="text/css" media="screen"> -- Node6 * <link href="12.css" rel="stylesheet" type="text/css" media="screen"> -- Node12 * <link href="13.css" rel="stylesheet" type="text/css" media="screen"> -- Node13 * * First we split to buckets bassed on the adjacency and other conditions. * buckets - [ [ Node1, Node2, Node3 ], [ Node4, Node 5 ], [ Node6 ], [ Node12, Node13 ] * Within each bucket we group them based on media type. * batches - [ Node1, Node2, Node3 ] --> [ [Node1, Node3], [Node2] ] * - [ Node4, Node 5 ] --> [ [ Node4, Node 5 ] ] * - [ Node6 ] --> [ [ Node6 ] ] * - [ Node12, Node13 ] --> [ [ Node12, Node13 ] ] * * Refer Tests for more examples. */ public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException { // Collate Elements into Buckets. List<List<Element>> concatBuckets = Lists.newLinkedList(); List<Element> curBucket = Lists.newLinkedList(); Iterator<Node> nodeIter = nodes.iterator(); Element cur = (Element) nodeIter.next(); curBucket.add(cur); while (nodeIter.hasNext()) { Element next = (Element) nodeIter.next(); if ((!split && cur != getSibling(next, true)) || (type == ConcatUriManager.Type.CSS && !areLinkNodesBucketable(cur, next))) { // Break off current bucket and add to list of all. concatBuckets.add(curBucket); curBucket = Lists.newLinkedList(); } curBucket.add(next); cur = next; } // Add leftovers. concatBuckets.add(curBucket); // Split the existing buckets based on media types into concat batches. List<List<Element>> concatBatches = Lists.newLinkedList(); Iterator<List<Element>> batchesIter = concatBuckets.iterator(); while (batchesIter.hasNext()) { splitBatchOnMedia(batchesIter.next(), concatBatches); } // Prepare batches of Uris to send to generate concat Uris List<List<Uri>> uriBatches = Lists.newLinkedList(); batchesIter = concatBatches.iterator(); while (batchesIter.hasNext()) { List<Element> batch = batchesIter.next(); List<Uri> uris = Lists.newLinkedList(); if (batch.isEmpty() || !getUris(type, batch, uris)) { batchesIter.remove(); continue; } uriBatches.add(uris); } if (uriBatches.isEmpty()) { return false; } // Generate the ConcatUris, then correlate with original elements. List<ConcatUriManager.ConcatData> concatUris = uriManager .make(ConcatUriManager.ConcatUri.fromList(gadget, uriBatches, type), !split); Iterator<List<Element>> elemBatchIt = concatBatches.iterator(); Iterator<List<Uri>> uriBatchIt = uriBatches.iterator(); for (ConcatUriManager.ConcatData concatUri : concatUris) { List<Element> sourceBatch = elemBatchIt.next(); List<Uri> sourceUris = uriBatchIt.next(); // Regardless what happens, inject a copy of the first node, // with new (concat) URI, immediately ahead of the first elem. Element firstElem = sourceBatch.get(0); Element elemConcat = (Element) firstElem.cloneNode(true); elemConcat.setAttribute(type.getSrcAttrib(), concatUri.getUri().toString()); firstElem.getParentNode().insertBefore(elemConcat, firstElem); // Now for all Elements, either A) remove them or B) replace each // with a <script> node with snippet of code configuring/evaluating // the resultant inserted code. This is useful for split-JS in particular, // and might also be used in spriting later. Iterator<Uri> uriIt = sourceUris.iterator(); for (Element elem : sourceBatch) { Uri elemOrigUri = uriIt.next(); String snippet = concatUri.getSnippet(elemOrigUri); if (!StringUtils.isEmpty(snippet)) { Node scriptNode = elem.getOwnerDocument().createElement("script"); scriptNode.setTextContent(snippet); elem.getParentNode().insertBefore(scriptNode, elem); } elem.getParentNode().removeChild(elem); } } return true; } /** * Split the given batch of elements (assumed to be sibling nodes that can be concatenated) * into batches with same media types. * * @param elements * @param output */ private void splitBatchOnMedia(List<Element> elements, List<List<Element>> output) { // Multimap to hold the ordered list of elements encountered for a given media type. Multimap<String, Element> mediaBatchMap = LinkedHashMultimap.create(); for (Element element : elements) { String mediaType = element.getAttribute("media"); mediaBatchMap.put(StringUtils.isEmpty(mediaType) ? "screen" : mediaType, element); } Set<String> mediaTypes = mediaBatchMap.keySet(); for (String mediaType : mediaTypes) { Collection<Element> elems = mediaBatchMap.get(mediaType); output.add(new LinkedList<Element>(elems)); } } private boolean isRewritableExternData(Element elem) { String uriStr = elem != null ? elem.getAttribute(type.getSrcAttrib()) : null; if (StringUtils.isEmpty(uriStr) || !config.shouldRewriteURL(uriStr)) { return false; } if (type == ConcatUriManager.Type.CSS) { // rel="stylesheet" and type="text/css" also required. return ("stylesheet".equalsIgnoreCase(elem.getAttribute("rel")) && "text/css".equalsIgnoreCase(elem.getAttribute("type"))); } return true; } private Element getSibling(Element root, boolean isPrev) { Node cur = root; while ((cur = getNext(cur, isPrev)) != null) { // Text nodes are safe to skip, as they won't effect styles or scripts. // It is also safe to skip comment nodes except for conditional comments. if (cur.getNodeType() == Node.TEXT_NODE || (cur.getNodeType() == Node.COMMENT_NODE && !isConditionalComment(cur))) { continue; } break; } if (cur != null && cur.getNodeType() == Node.ELEMENT_NODE) { return (Element) cur; } return null; } private Node getNext(Node node, boolean isPrev) { return isPrev ? node.getPreviousSibling() : node.getNextSibling(); } private boolean getUris(ConcatUriManager.Type type, List<Element> elems, List<Uri> uris) { for (Element elem : elems) { String uriStr = elem.getAttribute(type.getSrcAttrib()); try { uris.add(Uri.parse(uriStr)); } catch (Uri.UriException e) { // Invalid formatted Uri, batch failed. return false; } } return true; } /** * Checks if the css link tags can be put into the same bucket. */ private boolean areLinkNodesBucketable(Element current, Element next) { boolean areLinkNodesCompatible = false; // All link tags with media='all' should be placed in their own buckets. // Except for adjacent css links with media='all', which can belong to the // same bucket. String currMediaType = current.getAttribute("media"); String nextMediaType = next.getAttribute("media"); if (("all".equalsIgnoreCase(currMediaType) && "all".equalsIgnoreCase(nextMediaType)) || (!"all".equalsIgnoreCase(currMediaType) && !"all".equalsIgnoreCase(nextMediaType))) { areLinkNodesCompatible = true; } // we can't keep the link tags with different 'title' attribute in same // bucket. // An example that proves the above comment. // <link rel="stylesheet" type="text/css" href="a.css" /> // <link rel="stylesheet" type="text/css" href="b.css" title="small font"/> // <link rel="stylesheet" type="text/css" href="c.css" /> // <link rel="alternate stylesheet" type="text/css" href="d.css" title="large font"/> // Since browser allows to switch between the perferred styles 'small font' and 'large font', // we should not batch across the links with title attribute, as it will lead to reordering of // styles. return areLinkNodesCompatible && current.getAttribute("title").equals(next.getAttribute("title")); } /** * Checks if a given comment node is coditional comment. */ private boolean isConditionalComment(Node node) { return node.getNodeValue().trim().startsWith("[if"); } }