svn commit: r1430330 - in /lenya/branches/BRANCH_2_1_X/src/modules/resource: config/sitemap/ java/src/org/apache/lenya/cms/cocoon/ java/src/org/apache/lenya/cms/cocoon/selection/ java/src/org/apache/lenya/modules/resource/

[email protected] Tue, 08 Jan 2013 15:42:07 -0000
Newsgroups gmane.comp.cms.lenya.cvs
Message-ID <[email protected]>
Author: andreas
Date: Tue Jan  8 15:42:07 2013
New Revision: 1430330

URL: http://svn.apache.org/viewvc?rev=1430330&view=rev
Log:
Provide selector for using Accept header to determine resource representation.

Added:
    lenya/branches/BRANCH_2_1_X/src/modules/resource/config/sitemap/
    lenya/branches/BRANCH_2_1_X/src/modules/resource/config/sitemap/media-type-selector.xmap
    lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/
    lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/selection/
    lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/selection/MediaTypeSelector.java
    lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/modules/resource/MediaTypeParser.java

Added: lenya/branches/BRANCH_2_1_X/src/modules/resource/config/sitemap/media-type-selector.xmap
URL: http://svn.apache.org/viewvc/lenya/branches/BRANCH_2_1_X/src/modules/resource/config/sitemap/media-type-selector.xmap?rev=1430330&view=auto
==============================================================================
--- lenya/branches/BRANCH_2_1_X/src/modules/resource/config/sitemap/media-type-selector.xmap (added)
+++ lenya/branches/BRANCH_2_1_X/src/modules/resource/config/sitemap/media-type-selector.xmap Tue Jan  8 15:42:07 2013
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+
+<xmap xpath="/sitemap/components/selectors" 
+  unless="/sitemap/components/selectors/selector[@name = 'media-type']"
+  xmlns:map="http://apache.org/cocoon/sitemap/1.0">
+  <map:selector name="media-type" logger="lenya.sitemap.selector.mediatype"
+    src="org.apache.lenya.cms.cocoon.selection.MediaTypeSelector"/>
+</xmap>

Added: lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/selection/MediaTypeSelector.java
URL: http://svn.apache.org/viewvc/lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/selection/MediaTypeSelector.java?rev=1430330&view=auto
==============================================================================
--- lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/selection/MediaTypeSelector.java (added)
+++ lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/cms/cocoon/selection/MediaTypeSelector.java Tue Jan  8 15:42:07 2013
@@ -0,0 +1,58 @@
+/*
+ * 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.lenya.cms.cocoon.selection;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.selection.Selector;
+import org.apache.lenya.modules.resource.MediaTypeParser;
+
+import com.google.common.base.Optional;
+
+/**
+ * Compares the media type passed as expression to the Accept header of the HTTP request.
+ * Returns true if the media type matches. Matches with the double-wildcard (universal)
+ * media type are ignored.
+ */
+public class MediaTypeSelector extends AbstractLogEnabled implements Selector {
+
+    @Override
+    public boolean select(
+            final String expression,
+            @SuppressWarnings("rawtypes") final Map objectModel,
+            final Parameters parameters) {
+        
+        final Request request = ObjectModelHelper.getRequest(objectModel);
+        final Optional<String> type = MediaTypeParser.bestMatch(
+                Arrays.asList(expression.split("\\s,\\s")),
+                request.getHeader("Accept"),
+                true);
+        getLogger().info(
+                    "Accepted: " + expression
+                + "\nHeader:   " + request.getHeader("Accept")
+                + "\nDetected: " + type.or("-"));
+        return type.isPresent() && !type.equals("*/*");
+    }
+
+}

Added: lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/modules/resource/MediaTypeParser.java
URL: http://svn.apache.org/viewvc/lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/modules/resource/MediaTypeParser.java?rev=1430330&view=auto
==============================================================================
--- lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/modules/resource/MediaTypeParser.java (added)
+++ lenya/branches/BRANCH_2_1_X/src/modules/resource/java/src/org/apache/lenya/modules/resource/MediaTypeParser.java Tue Jan  8 15:42:07 2013
@@ -0,0 +1,250 @@
+/*
+ * 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.lenya.modules.resource;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.math.NumberUtils;
+
+import com.google.common.base.Optional;
+
+/**
+ * MIME-Type Parser
+ * 
+ * This class provides basic functions for handling mime-types. It can handle matching mime-types
+ * against a list of media-ranges. See section 14.1 of the HTTP specification [RFC 2616] for a
+ * complete explanation.
+ * 
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+ * 
+ * A port to Java of Joe Gregorio's MIME-Type Parser:
+ * 
+ * http://code.google.com/p/mimeparse/
+ * 
+ * Ported by Tom Zellman <[email protected]>.
+ */
+public final class MediaTypeParser {
+
+    /**
+     * Parse results container
+     */
+    protected static class ParseResults {
+        String type;
+
+        String subType;
+
+        // !a dictionary of all the parameters for the media range
+        Map<String, String> params;
+
+        @Override
+        public String toString() {
+            StringBuffer s = new StringBuffer("('" + type + "', '" + subType + "', {");
+            for (String k : params.keySet())
+                s.append("'" + k + "':'" + params.get(k) + "',");
+            return s.append("})").toString();
+        }
+    }
+
+    /**
+     * Carves up a mime-type and returns a ParseResults object
+     * 
+     * For example, the media range 'application/xhtml;q=0.5' would get parsed into:
+     * 
+     * ('application', 'xhtml', {'q', '0.5'})
+     */
+    protected static ParseResults parseMimeType(String mimeType) {
+        String[] parts = StringUtils.split(mimeType, ";");
+        ParseResults results = new ParseResults();
+        results.params = new HashMap<String, String>();
+
+        for (int i = 1; i < parts.length; ++i) {
+            String p = parts[i];
+            String[] subParts = StringUtils.split(p, '=');
+            if (subParts.length == 2)
+                results.params.put(subParts[0].trim(), subParts[1].trim());
+        }
+        String fullType = parts[0].trim();
+
+        // Java URLConnection class sends an Accept header that includes a
+        // single "*" - Turn it into a legal wildcard.
+        if (fullType.equals("*"))
+            fullType = "*/*";
+        String[] types = StringUtils.split(fullType, "/");
+        results.type = types[0].trim();
+        results.subType = types[1].trim();
+        return results;
+    }
+
+    /**
+     * Carves up a media range and returns a ParseResults.
+     * 
+     * For example, the media range 'application/*;q=0.5' would get parsed into:
+     * 
+     * ('application', '*', {'q', '0.5'})
+     * 
+     * In addition this function also guarantees that there is a value for 'q' in the params
+     * dictionary, filling it in with a proper default if necessary.
+     * 
+     * @param range
+     */
+    protected static ParseResults parseMediaRange(String range) {
+        ParseResults results = parseMimeType(range);
+        String q = results.params.get("q");
+        float f = NumberUtils.toFloat(q, 1);
+        if (StringUtils.isBlank(q) || f < 0 || f > 1)
+            results.params.put("q", "1");
+        return results;
+    }
+
+    /**
+     * Structure for holding a fitness/quality combo
+     */
+    protected static class FitnessAndQuality implements Comparable<FitnessAndQuality> {
+        int fitness;
+
+        float quality;
+
+        String mimeType; // optionally used
+
+        public FitnessAndQuality(int fitness, float quality) {
+            this.fitness = fitness;
+            this.quality = quality;
+        }
+
+        public int compareTo(FitnessAndQuality o) {
+            if (fitness == o.fitness) {
+                if (quality == o.quality)
+                    return 0;
+                else
+                    return quality < o.quality ? -1 : 1;
+            } else
+                return fitness < o.fitness ? -1 : 1;
+        }
+    }
+
+    /**
+     * Find the best match for a given mimeType against a list of media_ranges that have already
+     * been parsed by MimeParse.parseMediaRange(). Returns a tuple of the fitness value and the
+     * value of the 'q' quality parameter of the best match, or (-1, 0) if no match was found. Just
+     * as for quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges.
+     * 
+     * @param mimeType
+     * @param parsedRanges
+     */
+    protected static FitnessAndQuality fitnessAndQualityParsed(String mimeType,
+            Collection<ParseResults> parsedRanges) {
+        int bestFitness = -1;
+        float bestFitQ = 0;
+        ParseResults target = parseMediaRange(mimeType);
+
+        for (ParseResults range : parsedRanges) {
+            if ((target.type.equals(range.type) || range.type.equals("*") || target.type
+                    .equals("*"))
+                    && (target.subType.equals(range.subType) || range.subType.equals("*") || target.subType
+                            .equals("*"))) {
+                for (String k : target.params.keySet()) {
+                    int paramMatches = 0;
+                    if (!k.equals("q") && range.params.containsKey(k)
+                            && target.params.get(k).equals(range.params.get(k))) {
+                        paramMatches++;
+                    }
+                    int fitness = (range.type.equals(target.type)) ? 100 : 0;
+                    fitness += (range.subType.equals(target.subType)) ? 10 : 0;
+                    fitness += paramMatches;
+                    if (fitness > bestFitness) {
+                        bestFitness = fitness;
+                        bestFitQ = NumberUtils.toFloat(range.params.get("q"), 0);
+                    }
+                }
+            }
+        }
+        return new FitnessAndQuality(bestFitness, bestFitQ);
+    }
+
+    /**
+     * Find the best match for a given mime-type against a list of ranges that have already been
+     * parsed by parseMediaRange(). Returns the 'q' quality parameter of the best match, 0 if no
+     * match was found. This function bahaves the same as quality() except that 'parsed_ranges' must
+     * be a list of parsed media ranges.
+     * 
+     * @param mimeType
+     * @param parsedRanges
+     * @return
+     */
+    protected static float qualityParsed(String mimeType, Collection<ParseResults> parsedRanges) {
+        return fitnessAndQualityParsed(mimeType, parsedRanges).quality;
+    }
+
+    /**
+     * Returns the quality 'q' of a mime-type when compared against the mediaRanges in ranges. For
+     * example:
+     * 
+     * @param mimeType
+     * @param parsedRanges
+     */
+    public static float quality(String mimeType, String ranges) {
+        List<ParseResults> results = new LinkedList<ParseResults>();
+        for (String r : StringUtils.split(ranges, ','))
+            results.add(parseMediaRange(r));
+        return qualityParsed(mimeType, results);
+    }
+
+    /**
+     * Takes a list of supported mime-types and finds the best match for all the media-ranges listed
+     * in header. The value of header must be a string that conforms to the format of the HTTP
+     * Accept: header. The value of 'supported' is a list of mime-types.
+     * 
+     * MimeParse.bestMatch(Arrays.asList(new String[]{"application/xbel+xml", "text/xml"}),
+     * "text/*;q=0.5,*; q=0.1") 'text/xml'
+     * 
+     * @param supported
+     * @param header
+     * @return
+     */
+    public static Optional<String> bestMatch(Collection<String> supported, String header, boolean excludeAllType) {
+        List<ParseResults> parseResults = new LinkedList<ParseResults>();
+        List<FitnessAndQuality> weightedMatches = new LinkedList<FitnessAndQuality>();
+        for (String r : StringUtils.split(header, ',')) {
+            ParseResults result = parseMediaRange(r);
+            if (!(excludeAllType && result.type.equals("*") && result.subType.equals("*"))) {
+                parseResults.add(result);
+            }
+        }
+
+        for (String s : supported) {
+            FitnessAndQuality fitnessAndQuality = fitnessAndQualityParsed(s, parseResults);
+            fitnessAndQuality.mimeType = s;
+            weightedMatches.add(fitnessAndQuality);
+        }
+        Collections.sort(weightedMatches);
+
+        FitnessAndQuality lastOne = weightedMatches.get(weightedMatches.size() - 1);
+        return Optional.fromNullable(NumberUtils.compare(lastOne.quality, 0) != 0 ? lastOne.mimeType : null);
+    }
+
+    // hidden
+    private MediaTypeParser() {
+    }
+}