jackrabbit-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ju...@apache.org
Subject svn commit: r1023777 [1/2] - /jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/
Date Mon, 18 Oct 2010 12:55:40 GMT
Author: jukka
Date: Mon Oct 18 12:55:39 2010
New Revision: 1023777

URL: http://svn.apache.org/viewvc?rev=1023777&view=rev
Log:
JCR-2715: Improved join query performance

Use a more efficient join merging mechanism instead of the previous O(n^2) scan.

Added:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/OperandEvaluator.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java   (with props)
Modified:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java?rev=1023777&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java Mon Oct 18 12:55:39 2010
@@ -0,0 +1,84 @@
+/*
+ * 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.jackrabbit.core.query.lucene.join;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.ChildNodeJoinCondition;
+import javax.jcr.query.qom.Constraint;
+import javax.jcr.query.qom.Join;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+
+class ChildNodeJoinMerger extends JoinMerger {
+
+    private final String childSelector;
+
+    private final String parentSelector;
+
+    public ChildNodeJoinMerger(
+            Join join, Map<String, PropertyValue> columns,
+            OperandEvaluator evaluator, QueryObjectModelFactory factory,
+            ChildNodeJoinCondition condition)
+            throws RepositoryException {
+        super(join, columns, evaluator, factory);
+        this.childSelector = condition.getChildSelectorName();
+        this.parentSelector = condition.getParentSelectorName();
+    }
+
+    @Override
+    public Set<String> getLeftValues(Row row) throws RepositoryException {
+        return getValues(leftSelectors, row);
+    }
+
+    @Override
+    public Set<String> getRightValues(Row row) throws RepositoryException {
+        return getValues(rightSelectors, row);
+    }
+
+    @Override
+    public Constraint getRightJoinConstraint(List<Row> leftRows)
+            throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    private Set<String> getValues(Set<String> selectors, Row row)
+            throws RepositoryException {
+        if (selectors.contains(childSelector)) {
+            Node node = row.getNode(childSelector);
+            if (node != null && node.getDepth() > 0) {
+                return Collections.singleton(node.getParent().getPath());
+            }
+        } else if (selectors.contains(parentSelector)) {
+            Node node = row.getNode(parentSelector);
+            if (node != null) {
+                return Collections.singleton(node.getPath());
+            }
+        } else {
+            throw new RepositoryException("Invalid child node join");
+        }
+        return Collections.emptySet();
+    }
+
+}

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java?rev=1023777&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java Mon Oct 18 12:55:39 2010
@@ -0,0 +1,237 @@
+/*
+ * 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.jackrabbit.core.query.lucene.join;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.query.qom.And;
+import javax.jcr.query.qom.ChildNode;
+import javax.jcr.query.qom.Comparison;
+import javax.jcr.query.qom.Constraint;
+import javax.jcr.query.qom.DescendantNode;
+import javax.jcr.query.qom.DynamicOperand;
+import javax.jcr.query.qom.FullTextSearchScore;
+import javax.jcr.query.qom.Length;
+import javax.jcr.query.qom.LowerCase;
+import javax.jcr.query.qom.NodeLocalName;
+import javax.jcr.query.qom.NodeName;
+import javax.jcr.query.qom.Not;
+import javax.jcr.query.qom.Or;
+import javax.jcr.query.qom.PropertyExistence;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+import javax.jcr.query.qom.SameNode;
+import javax.jcr.query.qom.UpperCase;
+
+/**
+ * Returns a mapped constraint that only refers to the given set of
+ * selectors. The returned constraint is guaranteed to match an as small
+ * as possible superset of the node tuples matched by the given original
+ * constraints.
+ *
+ * @param constraint original constraint
+ * @param selectors target selectors
+ * @return mapped constraint
+ * @throws RepositoryException if the constraint mapping fails
+ */
+class ConstraintSplitter {
+
+    private final QueryObjectModelFactory factory;
+
+    private final Set<String> leftSelectors;
+
+    private final Set<String> rightSelectors;
+
+    private final List<Constraint> leftConstraints =
+        new ArrayList<Constraint>();
+
+    private final List<Constraint> rightConstraints =
+        new ArrayList<Constraint>();
+
+    public ConstraintSplitter(
+            Constraint constraint, QueryObjectModelFactory factory,
+            Set<String> leftSelectors, Set<String> rightSelectors)
+            throws RepositoryException {
+        this.factory = factory;
+        this.leftSelectors = leftSelectors;
+        this.rightSelectors = rightSelectors;
+
+        if (constraint != null) {
+            split(constraint);
+        }
+    }
+
+    /**
+     * @return the left constraint
+     */
+    public Constraint getLeftConstraint() throws RepositoryException {
+        return Constraints.and(factory,leftConstraints);
+    }
+
+    /**
+     * @return the right constraint
+     */
+    public Constraint getRightConstraint() throws RepositoryException {
+        return Constraints.and(factory, rightConstraints);
+    }
+
+    private void split(Constraint constraint) throws RepositoryException {
+        if (constraint instanceof Not) {
+            splitNot((Not) constraint);
+        } else if (constraint instanceof And) {
+            And and = (And) constraint;
+            split(and.getConstraint1());
+            split(and.getConstraint2());
+        } else {
+            splitBySelectors(constraint, getSelectorNames(constraint));
+        }
+    }
+
+    private void splitNot(Not not) throws RepositoryException {
+        Constraint constraint = not.getConstraint();
+        if (constraint instanceof Not) {
+            split(((Not) constraint).getConstraint());
+        } else if (constraint instanceof And) {
+            And and = (And) constraint;
+            split(factory.or(
+                    factory.not(and.getConstraint1()),
+                    factory.not(and.getConstraint2())));
+        } else if (constraint instanceof Or) {
+            Or or = (Or) constraint;
+            split(factory.and(
+                    factory.not(or.getConstraint1()),
+                    factory.not(or.getConstraint2())));
+        } else {
+            splitBySelectors(not, getSelectorNames(constraint));
+        }
+    }
+
+    private void splitBySelectors(Constraint constraint, Set<String> selectors)
+            throws UnsupportedRepositoryOperationException {
+        if (leftSelectors.containsAll(selectors)) {
+            leftConstraints.add(constraint);
+        } else if (rightSelectors.containsAll(selectors)) {
+            rightConstraints.add(constraint);
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unable to split a constraint that references"
+                    + " both sides of a join: " + constraint);
+        }
+    }
+
+    /**
+     * Returns the names of the selectors referenced by the given constraint.
+     *
+     * @param constraint constraint
+     * @return referenced selector names
+     * @throws UnsupportedRepositoryOperationException
+     *         if the constraint type is unknown
+     */
+    private Set<String> getSelectorNames(Constraint constraint)
+            throws UnsupportedRepositoryOperationException {
+        if (constraint instanceof And) {
+            And and = (And) constraint;
+            return getSelectorNames(and.getConstraint1(), and.getConstraint2());
+        } else if (constraint instanceof Or) {
+            Or or = (Or) constraint;
+            return getSelectorNames(or.getConstraint1(), or.getConstraint2());
+        } else if (constraint instanceof Not) {
+            Not not = (Not) constraint;
+            return getSelectorNames(not.getConstraint());
+        } else if (constraint instanceof PropertyExistence) {
+            PropertyExistence pe = (PropertyExistence) constraint;
+            return Collections.singleton(pe.getSelectorName());
+        } else if (constraint instanceof Comparison) {
+            Comparison c = (Comparison) constraint;
+            return Collections.singleton(getSelectorName(c.getOperand1()));
+        } else if (constraint instanceof SameNode) {
+            SameNode sn = (SameNode) constraint;
+            return Collections.singleton(sn.getSelectorName());
+        } else if (constraint instanceof ChildNode) {
+            ChildNode cn = (ChildNode) constraint;
+            return Collections.singleton(cn.getSelectorName());
+        } else if (constraint instanceof DescendantNode) {
+            DescendantNode dn = (DescendantNode) constraint;
+            return Collections.singleton(dn.getSelectorName());
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unknown constraint type: " + constraint);
+        }
+    }
+
+    /**
+     * Returns the combined set of selector names referenced by the given
+     * two constraint.
+     *
+     * @param a first constraint
+     * @param b second constraint
+     * @return selector names
+     * @throws UnsupportedRepositoryOperationException
+     *         if the constraint types are unknown
+     */
+    private Set<String> getSelectorNames(Constraint a, Constraint b)
+            throws UnsupportedRepositoryOperationException {
+        Set<String> set = new HashSet<String>();
+        set.addAll(getSelectorNames(a));
+        set.addAll(getSelectorNames(b));
+        return set;
+    }
+
+    /**
+     * Returns the selector name referenced by the given dynamic operand.
+     *
+     * @param operand dynamic operand
+     * @return selector name
+     * @throws UnsupportedRepositoryOperationException
+     *         if the operand type is unknown
+     */
+    private String getSelectorName(DynamicOperand operand)
+            throws UnsupportedRepositoryOperationException {
+        if (operand instanceof FullTextSearchScore) {
+            FullTextSearchScore ftss = (FullTextSearchScore) operand;
+            return ftss.getSelectorName();
+        } else if (operand instanceof Length) {
+            Length length = (Length) operand;
+            return getSelectorName(length.getPropertyValue());
+        } else if (operand instanceof LowerCase) {
+            LowerCase lower = (LowerCase) operand;
+            return getSelectorName(lower.getOperand());
+        } else if (operand instanceof NodeLocalName) {
+            NodeLocalName local = (NodeLocalName) operand;
+            return local.getSelectorName();
+        } else if (operand instanceof NodeName) {
+            NodeName name = (NodeName) operand;
+            return name.getSelectorName();
+        } else if (operand instanceof PropertyValue) {
+            PropertyValue value = (PropertyValue) operand;
+            return value.getSelectorName();
+        } else if (operand instanceof UpperCase) {
+            UpperCase upper = (UpperCase) operand;
+            return getSelectorName(upper.getOperand());
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unknown dynamic operand type: " + operand);
+        }
+    }
+
+}

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java?rev=1023777&r1=1023776&r2=1023777&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java Mon Oct 18 12:55:39 2010
@@ -16,62 +16,56 @@
  */
 package org.apache.jackrabbit.core.query.lucene.join;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import javax.jcr.RepositoryException;
 import javax.jcr.query.qom.Constraint;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 
 public class Constraints {
 
-    public static final Constraint TRUE = new Constraint() {};
-
-    public static final Constraint FALSE = new Constraint() {};
-
     public static Constraint and(
-            QueryObjectModelFactory factory, Constraint... constraints)
+            QueryObjectModelFactory factory, List<Constraint> constraints)
             throws RepositoryException {
-        Constraint constraint = TRUE;
-        for (int i = 0; constraints != null && i < constraints.length; i++) {
-            if (constraints[i] == FALSE) {
-                return FALSE;
-            } else if (constraints[i] != TRUE) {
-                if (constraint == TRUE) {
-                    constraint = constraints[i];
-                } else {
-                    constraint = factory.and(constraint, constraints[i]);
-                }
-            }
+        int n = constraints.size();
+        if (n == 0) {
+            return null;
+        } else if (n == 1) {
+            return constraints.get(0);
+        } else {
+            int m = n / 2;
+            return factory.and(
+                    and(factory, constraints.subList(0, m)),
+                    and(factory, constraints.subList(m + 1, n)));
         }
-        return constraint;
     }
 
-
-    public static Constraint or(
+    public static Constraint and(
             QueryObjectModelFactory factory, Constraint... constraints)
             throws RepositoryException {
-        Constraint constraint = FALSE;
-        for (int i = 0; constraints != null && i < constraints.length; i++) {
-            if (constraints[i] == TRUE) {
-                return TRUE;
-            } else if (constraints[i] != FALSE) {
-                if (constraint == FALSE) {
-                    constraint = constraints[i];
-                } else {
-                    constraint = factory.or(constraint, constraints[i]);
-                }
+        List<Constraint> list = new ArrayList<Constraint>(constraints.length);
+        for (Constraint constraint : constraints) {
+            if (constraint != null) {
+                list.add(constraint);
             }
         }
-        return constraint;
+        return and(factory, list);
     }
 
-    public static Constraint not(
-            QueryObjectModelFactory factory, Constraint constraint)
+    public static Constraint or(
+            QueryObjectModelFactory factory, List<Constraint> constraints)
             throws RepositoryException {
-        if (constraint == TRUE) {
-            return FALSE;
-        } else if (constraint == FALSE) {
-            return TRUE;
+        int n = constraints.size();
+        if (n == 0) {
+            return null;
+        } else if (n == 1) {
+            return constraints.get(0);
         } else {
-            return factory.not(constraint);
+            int m = n / 2;
+            return factory.or(
+                    or(factory, constraints.subList(0, m)),
+                    or(factory, constraints.subList(m + 1, n)));
         }
     }
 

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java?rev=1023777&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java Mon Oct 18 12:55:39 2010
@@ -0,0 +1,91 @@
+/*
+ * 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.jackrabbit.core.query.lucene.join;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.Constraint;
+import javax.jcr.query.qom.DescendantNodeJoinCondition;
+import javax.jcr.query.qom.EquiJoinCondition;
+import javax.jcr.query.qom.Join;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+
+class DescendantNodeJoinMerger extends JoinMerger {
+
+    private final String descendantSelector;
+
+    private final String ancestorSelector;
+
+    public DescendantNodeJoinMerger(
+            Join join, Map<String, PropertyValue> columns,
+            OperandEvaluator evaluator, QueryObjectModelFactory factory,
+            DescendantNodeJoinCondition condition)
+            throws RepositoryException {
+        super(join, columns, evaluator, factory);
+        this.descendantSelector = condition.getDescendantSelectorName();
+        this.ancestorSelector = condition.getAncestorSelectorName();
+    }
+
+    @Override
+    public Set<String> getLeftValues(Row row) throws RepositoryException {
+        return getValues(leftSelectors, row);
+    }
+
+    @Override
+    public Set<String> getRightValues(Row row) throws RepositoryException {
+        return getValues(rightSelectors, row);
+    }
+
+    @Override
+    public Constraint getRightJoinConstraint(List<Row> leftRows)
+            throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    private Set<String> getValues(Set<String> selectors, Row row)
+            throws RepositoryException {
+        if (selectors.contains(descendantSelector)) {
+            Node node = row.getNode(descendantSelector);
+            if (node != null) {
+                Set<String> values = new HashSet<String>();
+                while (node.getDepth() > 0) {
+                    node = node.getParent();
+                    values.add(node.getPath());
+                }
+                return values;
+            }
+        } else if (selectors.contains(ancestorSelector)) {
+            Node node = row.getNode(ancestorSelector);
+            if (node != null) {
+                return Collections.singleton(node.getPath());
+            }
+        } else {
+            throw new RepositoryException("Invalid descendant node join");
+        }
+        return Collections.emptySet();
+    }
+
+}

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java?rev=1023777&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java Mon Oct 18 12:55:39 2010
@@ -0,0 +1,107 @@
+/*
+ * 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.jackrabbit.core.query.lucene.join;
+
+import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.Constraint;
+import javax.jcr.query.qom.EquiJoinCondition;
+import javax.jcr.query.qom.Join;
+import javax.jcr.query.qom.Literal;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+
+class EquiJoinMerger extends JoinMerger {
+
+    private final PropertyValue leftProperty;
+
+    private final PropertyValue rightProperty;
+
+    public EquiJoinMerger(
+            Join join, Map<String, PropertyValue> columns,
+            OperandEvaluator evaluator, QueryObjectModelFactory factory,
+            EquiJoinCondition condition) throws RepositoryException {
+        super(join, columns, evaluator, factory);
+
+        PropertyValue property1 = factory.propertyValue(
+                condition.getSelector1Name(), condition.getProperty1Name());
+        PropertyValue property2 = factory.propertyValue(
+                condition.getSelector2Name(), condition.getProperty2Name());
+
+        if (leftSelectors.contains(property1.getSelectorName())
+                && rightSelectors.contains(property2.getSelectorName())) {
+            leftProperty = property1;
+            rightProperty = property2;
+        } else if (leftSelectors.contains(property2.getSelectorName())
+                && rightSelectors.contains(property1.getSelectorName())) {
+            leftProperty = property1;
+            rightProperty = property2;
+        } else {
+            throw new RepositoryException("Invalid equi-join");
+        }
+    }
+
+    @Override
+    public Set<String> getLeftValues(Row row) throws RepositoryException {
+        return getValues(leftProperty, row);
+    }
+
+    @Override
+    public Set<String> getRightValues(Row row) throws RepositoryException {
+        return getValues(rightProperty, row);
+    }
+
+    @Override
+    public Constraint getRightJoinConstraint(List<Row> leftRows)
+            throws RepositoryException {
+        Map<String, Literal> literals = new HashMap<String, Literal>();
+        for (Row leftRow : leftRows) {
+            for (Value value : evaluator.getValues(leftProperty, leftRow)) {
+                literals.put(value.getString(), factory.literal(value));
+            }
+        }
+
+        List<Constraint> constraints =
+            new ArrayList<Constraint>(literals.size());
+        for (Literal literal : literals.values()) {
+            constraints.add(factory.comparison(
+                    rightProperty, JCR_OPERATOR_EQUAL_TO, literal));
+        }
+
+        return Constraints.or(factory, constraints);
+    }
+
+    private Set<String> getValues(PropertyValue property, Row row)
+            throws RepositoryException {
+        Set<String> strings = new HashSet<String>();
+        for (Value value : evaluator.getValues(property, row)) {
+            strings.add(value.getString());
+        }
+        return strings;
+    }
+
+}

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java?rev=1023777&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java Mon Oct 18 12:55:39 2010
@@ -0,0 +1,263 @@
+/*
+ * 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.jackrabbit.core.query.lucene.join;
+
+import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER;
+import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.Value;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.Row;
+import javax.jcr.query.RowIterator;
+import javax.jcr.query.qom.ChildNodeJoinCondition;
+import javax.jcr.query.qom.Constraint;
+import javax.jcr.query.qom.DescendantNodeJoinCondition;
+import javax.jcr.query.qom.EquiJoinCondition;
+import javax.jcr.query.qom.Join;
+import javax.jcr.query.qom.JoinCondition;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+import javax.jcr.query.qom.SameNodeJoinCondition;
+import javax.jcr.query.qom.Selector;
+import javax.jcr.query.qom.Source;
+
+import org.apache.jackrabbit.commons.iterator.RowIterable;
+import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter;
+
+abstract class JoinMerger {
+
+    public static JoinMerger getJoinMerger(
+            Join join, Map<String, PropertyValue> columns,
+            OperandEvaluator evaluator, QueryObjectModelFactory factory)
+            throws RepositoryException {
+        JoinCondition condition = join.getJoinCondition();
+        if (condition instanceof EquiJoinCondition) {
+            return new EquiJoinMerger(
+                    join, columns, evaluator, factory,
+                    (EquiJoinCondition) condition);
+        } else if (condition instanceof SameNodeJoinCondition) {
+            return new SameNodeJoinMerger(
+                    join, columns, evaluator, factory,
+                    (SameNodeJoinCondition) condition);
+        } else if (condition instanceof ChildNodeJoinCondition) {
+            return new ChildNodeJoinMerger(
+                    join, columns, evaluator, factory,
+                    (ChildNodeJoinCondition) condition);
+        } else if (condition instanceof DescendantNodeJoinCondition) {
+            return new DescendantNodeJoinMerger(
+                    join, null, evaluator, factory,
+                    (DescendantNodeJoinCondition) condition);
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unsupported join condition type: " + condition);
+        }
+    }
+
+    private final String type;
+
+    protected final Set<String> leftSelectors;
+
+    protected final Set<String> rightSelectors;
+
+    private final String[] selectorNames;
+
+    private final String[] columnNames;
+
+    private final PropertyValue[] operands;
+
+    protected final OperandEvaluator evaluator;
+
+    protected final QueryObjectModelFactory factory;
+
+    protected JoinMerger(
+            Join join, Map<String, PropertyValue> columns,
+            OperandEvaluator evaluator, QueryObjectModelFactory factory)
+            throws RepositoryException {
+        this.type = join.getJoinType();
+
+        this.leftSelectors = getSelectorNames(join.getLeft());
+        this.rightSelectors = getSelectorNames(join.getRight());
+
+        Set<String> selectors = new LinkedHashSet<String>();
+        selectors.addAll(leftSelectors);
+        selectors.addAll(rightSelectors);
+        this.selectorNames =
+            selectors.toArray(new String[selectors.size()]);
+
+        this.columnNames =
+            columns.keySet().toArray(new String[columns.size()]);
+        this.operands =
+            columns.values().toArray(new PropertyValue[columns.size()]);
+
+        this.evaluator = evaluator;
+        this.factory = factory;
+    }
+
+    public Set<String> getLeftSelectors() {
+        return leftSelectors;
+    }
+
+    public Set<String> getRightSelectors() {
+        return rightSelectors;
+    }
+
+    private Set<String> getSelectorNames(Source source)
+            throws RepositoryException {
+        if (source instanceof Selector) {
+            Selector selector = (Selector) source;
+            return Collections.singleton(selector.getSelectorName());
+        } else if (source instanceof Join) {
+            Join join = (Join) source;
+            Set<String> set = new LinkedHashSet<String>();
+            set.addAll(getSelectorNames(join.getLeft()));
+            set.addAll(getSelectorNames(join.getRight()));
+            return set;
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unknown source type: " + source);
+        }
+    }
+
+    public QueryResult merge(
+            RowIterator leftRows, RowIterator rightRows)
+            throws RepositoryException {
+        if (JCR_JOIN_TYPE_RIGHT_OUTER.equals(type)) {
+            Map<String, List<Row>> map = new HashMap<String, List<Row>>();
+            for (Row row : new RowIterable(leftRows)) {
+                for (String value : getLeftValues(row)) {
+                    List<Row> rows = map.get(value);
+                    if (rows == null) {
+                        rows = new ArrayList<Row>();
+                        map.put(value, rows);
+                    }
+                    rows.add(row);
+                }
+            }
+            return mergeRight(map, rightRows);
+        } else {
+            Map<String, List<Row>> map = new HashMap<String, List<Row>>();
+            for (Row row : new RowIterable(leftRows)) {
+                for (String value : getLeftValues(row)) {
+                    List<Row> rows = map.get(value);
+                    if (rows == null) {
+                        rows = new ArrayList<Row>();
+                        map.put(value, rows);
+                    }
+                    rows.add(row);
+                }
+            }
+            boolean outer = JCR_JOIN_TYPE_LEFT_OUTER.equals(type);
+            return mergeLeft(leftRows, map, outer);
+        }
+    }
+
+    private QueryResult mergeLeft(
+            RowIterator leftRows, Map<String, List<Row>> rightRowMap,
+            boolean outer) throws RepositoryException {
+        List<Row> rows = new ArrayList<Row>();
+        for (Row leftRow : new RowIterable(leftRows)) {
+            for (String value : getLeftValues(leftRow)) {
+                List<Row> rightRows = rightRowMap.get(value);
+                if (leftRows != null) {
+                    for (Row rightRow : rightRows) {
+                        rows.add(mergeRow(leftRow, rightRow));
+                    }
+                } else if (outer) {
+                    rows.add(mergeRow(leftRow, null));
+                }
+            }
+        }
+        return new SimpleQueryResult(
+                columnNames, selectorNames, new RowIteratorAdapter(rows));
+    }
+
+    private QueryResult mergeRight(
+            Map<String, List<Row>> leftRowMap, RowIterator rightRows)
+            throws RepositoryException {
+        List<Row> rows = new ArrayList<Row>();
+        for (Row rightRow : new RowIterable(rightRows)) {
+            for (String value : getRightValues(rightRow)) {
+                List<Row> leftRows = leftRowMap.get(value);
+                if (leftRows != null) {
+                    for (Row leftRow : leftRows) {
+                        rows.add(mergeRow(leftRow, rightRow));
+                    }
+                } else {
+                    rows.add(mergeRow(null, rightRow));
+                }
+            }
+        }
+        return new SimpleQueryResult(
+                columnNames, selectorNames, new RowIteratorAdapter(rows));
+    }
+
+    /**
+     * Merges the given left and right rows to a single joined row.
+     *
+     * @param left left row, possibly <code>null</code> in a right outer join
+     * @param right right row, possibly <code>null</code> in a left outer join
+     * @return joined row
+     * @throws RepositoryException if the rows can't be joined
+     */
+    private Row mergeRow(Row left, Row right) throws RepositoryException {
+        Node[] nodes = new Node[selectorNames.length];
+        double[] scores = new double[selectorNames.length];
+        for (int i = 0; i < selectorNames.length; i++) {
+            String selector = selectorNames[i];
+            if (left != null && leftSelectors.contains(selector)) {
+                nodes[i] = left.getNode(selector);
+                scores[i] = left.getScore(selector);
+            } else if (right != null && rightSelectors.contains(selector)) {
+                nodes[i] = right.getNode(selector);
+                scores[i] = right.getScore(selector);
+            } else {
+                nodes[i] = null;
+                scores[i] = 0.0;
+            }
+        }
+
+        Value[] values = new Value[operands.length];
+        Row row = new SimpleRow(
+                columnNames, values, selectorNames, nodes, scores);
+        for (int i = 0; i < operands.length; i++) {
+            values[i] = evaluator.getValue(operands[i], row);
+        }
+
+        return row;
+    }
+
+    public abstract Set<String> getLeftValues(Row row)
+            throws RepositoryException;
+
+    public abstract Set<String> getRightValues(Row row)
+            throws RepositoryException;
+
+    public abstract Constraint getRightJoinConstraint(List<Row> leftRows)
+            throws RepositoryException;
+
+}

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/OperandEvaluator.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/OperandEvaluator.java?rev=1023777&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/OperandEvaluator.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/OperandEvaluator.java Mon Oct 18 12:55:39 2010
@@ -0,0 +1,292 @@
+/*
+ * 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.jackrabbit.core.query.lucene.join;
+
+import static java.util.Locale.ENGLISH;
+import static javax.jcr.PropertyType.NAME;
+
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+import javax.jcr.query.Row;
+import javax.jcr.query.qom.BindVariableValue;
+import javax.jcr.query.qom.FullTextSearchScore;
+import javax.jcr.query.qom.Length;
+import javax.jcr.query.qom.Literal;
+import javax.jcr.query.qom.LowerCase;
+import javax.jcr.query.qom.NodeLocalName;
+import javax.jcr.query.qom.NodeName;
+import javax.jcr.query.qom.Operand;
+import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.UpperCase;
+
+class OperandEvaluator {
+
+    private final ValueFactory factory;
+
+    private final Map<String, Value> variables;
+
+    public OperandEvaluator(
+            ValueFactory factory, Map<String, Value> variables) {
+        this.factory = factory;
+        this.variables = variables;
+    }
+
+    public Value getValue(Operand operand, Row row) throws RepositoryException {
+        Value[] values = getValues(operand, row);
+        switch (values.length) {
+        case 0:
+            return factory.createValue("");
+        case 1:
+            return values[0];
+        default:
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < values.length; i++) {
+                if (i > 0) {
+                    builder.append(' ');
+                }
+                builder.append(values[i].getString());
+            }
+            return factory.createValue(builder.toString());
+        }
+    }
+
+    /**
+     * Evaluates the given operand against the given row.
+     *
+     * @param operand operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    public Value[] getValues(Operand operand, Row row)
+            throws RepositoryException {
+        if (operand instanceof BindVariableValue) {
+            return getBindVariableValues((BindVariableValue) operand);
+        } else if (operand instanceof FullTextSearchScore) {
+            return getFullTextSearchScoreValues(
+                    (FullTextSearchScore) operand, row);
+        } else if (operand instanceof Length) {
+            return getLengthValues((Length) operand, row);
+        } else if (operand instanceof Literal) {
+            return getLiteralValues((Literal) operand);
+        } else if (operand instanceof LowerCase) {
+            return getLowerCaseValues((LowerCase) operand, row);
+        } else if (operand instanceof NodeLocalName) {
+            return getNodeLocalNameValues((NodeLocalName) operand, row);
+        } else if (operand instanceof NodeName) {
+            return getNodeNameValues((NodeName) operand, row);
+        } else if (operand instanceof PropertyValue) {
+            return getPropertyValues((PropertyValue) operand, row);
+        } else if (operand instanceof UpperCase) {
+            return getUpperCaseValues((UpperCase) operand, row);
+        } else {
+            throw new UnsupportedRepositoryOperationException(
+                    "Unknown operand type: " + operand);
+        }
+    }
+
+    /**
+     * Returns the value of the given variable value operand at the given row.
+     *
+     * @param operand variable value operand
+     * @return value of the operand at the given row
+     */
+    private Value[] getBindVariableValues(BindVariableValue operand) {
+        Value value = variables.get(operand.getBindVariableName());
+        if (value != null) {
+            return new Value[] { value };
+        } else {
+            return new Value[0];
+        }
+    }
+
+    /**
+     * Returns the value of the given search score operand at the given row.
+     *
+     * @param operand search score operand
+     * @param row row
+     * @return value of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getFullTextSearchScoreValues(
+            FullTextSearchScore operand, Row row) throws RepositoryException {
+        double score = row.getScore(operand.getSelectorName());
+        return new Value[] { factory.createValue(score) };
+    }
+
+    /**
+     * Returns the values of the given value length operand at the given row.
+     *
+     * @see #getProperty(PropertyValue, Row)
+     * @param operand value length operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getLengthValues(Length operand, Row row)
+            throws RepositoryException {
+        Property property = getProperty(operand.getPropertyValue(), row);
+        if (property == null) {
+            return new Value[0];
+        } else if (property.isMultiple()) {
+            long[] lengths = property.getLengths();
+            Value[] values = new Value[lengths.length];
+            for (int i = 0; i < lengths.length; i++) {
+                values[i] = factory.createValue(lengths[i]);
+            }
+            return values;
+        } else {
+            long length = property.getLength();
+            return new Value[] { factory.createValue(length) };
+        }
+    }
+
+    /**
+     * Returns the value of the given literal value operand.
+     *
+     * @param operand literal value operand
+     * @return value of the operand
+     */
+    private Value[] getLiteralValues(Literal operand) {
+        return new Value[] { operand.getLiteralValue() };
+    }
+
+    /**
+     * Returns the values of the given lower case operand at the given row.
+     *
+     * @param operand lower case operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getLowerCaseValues(LowerCase operand, Row row)
+            throws RepositoryException {
+        Value[] values = getValues(operand.getOperand(), row);
+        for (int i = 0; i < values.length; i++) {
+            String value = values[i].getString();
+            String lower = value.toLowerCase(ENGLISH);
+            if (!value.equals(lower)) {
+                values[i] = factory.createValue(lower);
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Returns the value of the given local name operand at the given row.
+     *
+     * @param operand local name operand
+     * @param row row
+     * @return value of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getNodeLocalNameValues(NodeLocalName operand, Row row)
+            throws RepositoryException {
+        String name = row.getNode(operand.getSelectorName()).getName();
+        int colon = name.indexOf(':');
+        if (colon != -1) {
+            name = name.substring(colon + 1);
+        }
+        return new Value[] { factory.createValue(name, NAME) };
+    }
+
+    /**
+     * Returns the value of the given node name operand at the given row.
+     *
+     * @param operand node name operand
+     * @param row row
+     * @return value of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getNodeNameValues(NodeName operand, Row row)
+            throws RepositoryException {
+        Node node = row.getNode(operand.getSelectorName());
+        return new Value[] { factory.createValue(node.getName(), NAME) };
+    }
+
+    /**
+     * Returns the values of the given property value operand at the given row.
+     *
+     * @see #getProperty(PropertyValue, Row)
+     * @param operand property value operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getPropertyValues(PropertyValue operand, Row row)
+            throws RepositoryException {
+        Property property = getProperty(operand, row);
+        if (property == null) {
+            return new Value[0];
+        } else if (property.isMultiple()) {
+            return property.getValues();
+        } else {
+            return new Value[] { property.getValue() };
+        }
+    }
+
+    /**
+     * Returns the values of the given upper case operand at the given row.
+     *
+     * @param operand upper case operand
+     * @param row row
+     * @return values of the operand at the given row
+     * @throws RepositoryException if the operand can't be evaluated
+     */
+    private Value[] getUpperCaseValues(UpperCase operand, Row row)
+            throws RepositoryException {
+        Value[] values = getValues(operand.getOperand(), row);
+        for (int i = 0; i < values.length; i++) {
+            String value = values[i].getString();
+            String upper = value.toLowerCase(ENGLISH);
+            if (!value.equals(upper)) {
+                values[i] = factory.createValue(upper);
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Returns the identified property from the given row. This method
+     * is used by both the {@link #getValue(Length, Row)} and the
+     * {@link #getValue(PropertyValue, Row)} methods to access properties.
+     *
+     * @param operand property value operand
+     * @param row row
+     * @return the identified property,
+     *         or <code>null</code> if the property does not exist
+     * @throws RepositoryException if the property can't be accessed
+     */
+    private Property getProperty(PropertyValue operand, Row row)
+            throws RepositoryException {
+        try {
+            String selector = operand.getSelectorName();
+            String property = operand.getPropertyName();
+            return row.getNode(selector).getProperty(property);
+        } catch (PathNotFoundException e) {
+            return null;
+        }
+    }
+
+}

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/OperandEvaluator.java
------------------------------------------------------------------------------
    svn:eol-style = native



Mime
View raw message