jackrabbit-oak-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Alex Parvulescu <alex.parvule...@gmail.com>
Subject Re: svn commit: r1373392 - in /jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user: ./ query/
Date Fri, 17 Aug 2012 09:13:12 GMT
Hi Angela,

I fully agree with you about the need to have this code in oak.
That is why my email started with...

> I'd like to raise a concern

XPath to SQL2 conversion is a WIP, the level of support for XPath queries
is still under discussion, which makes the platform you are building the
user parts on unstable.
I just wanted to make sure that the risk is known.

On the other hand, I'm sure that the extensive range of tests that cover
this functionality assures us that there are no obvious problems.

> do you volunteer to take care of that? that would be perfect.
I'll create an issue to make sure we don't forget to come back to the code
later on.


thanks,
alex


On Fri, Aug 17, 2012 at 11:01 AM, Angela Schreiber <anchela@adobe.com>wrote:

> hi alex
>
> for backward compatibility the form jr-user-query implementation
> and the authorizable-query-utility present in jackrabbit-jcr-commons
> is required to work as is as nobody will have time to fix all
> usages of that in our products.
>
> we may - just in case we have time left - add a additional implementation
> of michael's user-query API and start deprecating
> the old one in a subsequent release of oak. do you volunteer
> to take care of that? that would be perfect.
>
> kind regards
> angela
>
>
> On 8/17/12 10:43 AM, Alex Parvulescu wrote:
>
>> Hi,
>>
>> I'd like to raise a concern here about the XPath query builder that made
>> its way into oak-jcr with this commit.
>>
>> There is no native XPath support in Oak. Currently the XPath queries are
>> beaing translated into (more or less) equivalent SQL2 queries. See
>> also OAK-225.
>> So under these circumstances it doesn't make sense to build a query
>> programatically as XPath just to have it translated into SQL2 at a later
>> stage.
>>
>> thoughts?
>>
>> thanks,
>> alex
>>
>>
>> On Wed, Aug 15, 2012 at 3:24 PM,<angela@apache.org>  wrote:
>>
>>  Author: angela
>>> Date: Wed Aug 15 13:24:21 2012
>>> New Revision: 1373392
>>>
>>> URL: http://svn.apache.org/viewvc?**rev=1373392&view=rev<http://svn.apache.org/viewvc?rev=1373392&view=rev>
>>> Log:
>>> OAK-50 : Implement User Management (WIP)
>>>
>>> Added:
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/Condition.java
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ConditionVisitor.**java
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/RelationOp.java
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ResultIterator.java
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/XPathQueryBuilder.**java
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/**XPathQueryEvaluator.java
>>> Modified:
>>>
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/UserManagerImpl.java
>>>
>>> Modified:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/UserManagerImpl.java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**
>>> user/UserManagerImpl.java?rev=**1373392&r1=1373391&r2=1373392&**
>>> view=diff<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerImpl.java?rev=1373392&r1=1373391&r2=1373392&view=diff>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/UserManagerImpl.java
>>> (original)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/UserManagerImpl.java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -41,6 +41,8 @@ import org.apache.jackrabbit.oak.api.**Pro<http://org.apache.jackrabbit.oak.api.Pro>
>>>   import org.apache.jackrabbit.oak.api.**Root;
>>>   import org.apache.jackrabbit.oak.api.**Tree;
>>>   import org.apache.jackrabbit.oak.jcr.**SessionDelegate;
>>> +import
>>> org.apache.jackrabbit.oak.jcr.**security.user.query.**XPathQueryBuilder;
>>> +import
>>> org.apache.jackrabbit.oak.jcr.**security.user.query.**
>>> XPathQueryEvaluator;
>>>   import org.apache.jackrabbit.oak.jcr.**value.ValueConverter;
>>>   import org.apache.jackrabbit.oak.**security.user.**UserProviderImpl;
>>>   import org.apache.jackrabbit.oak.spi.**security.principal.**
>>> EveryonePrincipal;
>>> @@ -142,8 +144,9 @@ public class UserManagerImpl implements
>>>
>>>       @Override
>>>       public Iterator<Authorizable>  findAuthorizables(Query query)
>>> throws
>>> RepositoryException {
>>> -        // TODO : execute the specified query
>>> -        throw new UnsupportedOperationException(**"Not Implemented");
>>> +        XPathQueryBuilder builder = new XPathQueryBuilder();
>>> +        query.build(builder);
>>> +        return new XPathQueryEvaluator(builder, this,
>>> sessionDelegate.**getQueryManager(),
>>> sessionDelegate.**getNamePathMapper()).eval();
>>>       }
>>>
>>>       @Override
>>>
>>> Added:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/Condition.java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**
>>> user/query/Condition.java?rev=**1373392&view=auto<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/query/Condition.java?rev=1373392&view=auto>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/Condition.java
>>> (added)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/Condition.java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -0,0 +1,190 @@
>>> +/*
>>> + * 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<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.oak.jcr.**security.user.query;
>>> +
>>> +import java.util.ArrayList;
>>> +import java.util.Iterator;
>>> +import java.util.List;
>>> +import javax.jcr.RepositoryException;
>>> +import javax.jcr.Value;
>>> +
>>> +
>>> +interface Condition {
>>> +
>>> +    void accept(ConditionVisitor visitor) throws RepositoryException;
>>> +
>>> +    //----------------------------**--------------<  Condition
>>> implementations>---
>>> +
>>> +    static class Node implements Condition {
>>> +        private final String pattern;
>>> +
>>> +        public Node(String pattern) {
>>> +            this.pattern = pattern;
>>> +        }
>>> +
>>> +        public String getPattern() {
>>> +            return pattern;
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) throws
>>> RepositoryException {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +
>>> +    static class Property implements Condition {
>>> +        private final String relPath;
>>> +        private final RelationOp op;
>>> +        private final Value value;
>>> +        private final String pattern;
>>> +
>>> +        public Property(String relPath, RelationOp op, Value value) {
>>> +            this.relPath = relPath;
>>> +            this.op = op;
>>> +            this.value = value;
>>> +            pattern = null;
>>> +        }
>>> +
>>> +        public Property(String relPath, RelationOp op, String pattern) {
>>> +            this.relPath = relPath;
>>> +            this.op = op;
>>> +            value = null;
>>> +            this.pattern = pattern;
>>> +        }
>>> +
>>> +        public Property(String relPath, RelationOp op) {
>>> +            this.relPath = relPath;
>>> +            this.op = op;
>>> +            value = null;
>>> +            pattern = null;
>>> +        }
>>> +
>>> +        public String getRelPath() {
>>> +            return relPath;
>>> +        }
>>> +
>>> +        public RelationOp getOp() {
>>> +            return op;
>>> +        }
>>> +
>>> +        public Value getValue() {
>>> +            return value;
>>> +        }
>>> +
>>> +        public String getPattern() {
>>> +            return pattern;
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) throws
>>> RepositoryException {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +
>>> +    static class Contains implements Condition {
>>> +        private final String relPath;
>>> +        private final String searchExpr;
>>> +
>>> +        public Contains(String relPath, String searchExpr) {
>>> +            this.relPath = relPath;
>>> +            this.searchExpr = searchExpr;
>>> +        }
>>> +
>>> +        public String getRelPath() {
>>> +            return relPath;
>>> +        }
>>> +
>>> +        public String getSearchExpr() {
>>> +            return searchExpr;
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +
>>> +    static class Impersonation implements Condition {
>>> +        private final String name;
>>> +
>>> +        public Impersonation(String name) {
>>> +            this.name = name;
>>> +        }
>>> +
>>> +        public String getName() {
>>> +            return name;
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +
>>> +    static class Not implements Condition {
>>> +        private final Condition condition;
>>> +
>>> +        public Not(Condition condition) {
>>> +            this.condition = condition;
>>> +        }
>>> +
>>> +        public Condition getCondition() {
>>> +            return condition;
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) throws
>>> RepositoryException {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +
>>> +    abstract static class Compound implements Condition,
>>> Iterable<Condition>  {
>>> +        private final List<Condition>  conditions = new
>>> ArrayList<Condition>();
>>> +
>>> +        public Compound() {
>>> +            super();
>>> +        }
>>> +
>>> +        public Compound(Condition condition1, Condition condition2) {
>>> +            conditions.add(condition1);
>>> +            conditions.add(condition2);
>>> +        }
>>> +
>>> +        public void addCondition(Condition condition) {
>>> +            conditions.add(condition);
>>> +        }
>>> +
>>> +        public Iterator<Condition>  iterator() {
>>> +            return conditions.iterator();
>>> +        }
>>> +    }
>>> +
>>> +    static class And extends Compound {
>>> +        public And(Condition condition1, Condition condition2) {
>>> +            super(condition1, condition2);
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) throws
>>> RepositoryException {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +
>>> +    static class Or extends Compound {
>>> +        public Or(Condition condition1, Condition condition2) {
>>> +            super(condition1, condition2);
>>> +        }
>>> +
>>> +        public void accept(ConditionVisitor visitor) throws
>>> RepositoryException {
>>> +            visitor.visit(this);
>>> +        }
>>> +    }
>>> +}
>>> \ No newline at end of file
>>>
>>> Added:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ConditionVisitor.**java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**
>>> user/query/ConditionVisitor.**java?rev=1373392&view=auto<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/query/ConditionVisitor.java?rev=1373392&view=auto>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ConditionVisitor.**java
>>> (added)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ConditionVisitor.**java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -0,0 +1,36 @@
>>> +/*
>>> + * 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<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.oak.jcr.**security.user.query;
>>> +
>>> +import javax.jcr.RepositoryException;
>>> +
>>> +interface ConditionVisitor {
>>> +
>>> +    void visit(Condition.Node node) throws RepositoryException;
>>> +
>>> +    void visit(Condition.Property condition) throws RepositoryException;
>>> +
>>> +    void visit(Condition.Contains condition);
>>> +
>>> +    void visit(Condition.Impersonation condition);
>>> +
>>> +    void visit(Condition.Not condition) throws RepositoryException;
>>> +
>>> +    void visit(Condition.And condition) throws RepositoryException;
>>> +
>>> +    void visit(Condition.Or condition) throws RepositoryException;
>>> +}
>>> \ No newline at end of file
>>>
>>> Added:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/RelationOp.java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**
>>> user/query/RelationOp.java?**rev=1373392&view=auto<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/query/RelationOp.java?rev=1373392&view=auto>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/RelationOp.java
>>> (added)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/RelationOp.java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -0,0 +1,28 @@
>>> +package org.apache.jackrabbit.oak.jcr.**security.user.query;
>>> +
>>> +/**
>>> + * Relational operators for comparing a property to a value. Correspond
>>> + * to the general comparison operators as define in JSR-170.
>>> + * The {@link #EX} tests for existence of a property.
>>> + */
>>> +enum RelationOp {
>>> +
>>> +    NE("!="),
>>> +    EQ("="),
>>> +    LT("<"),
>>> +    LE("<="),
>>> +    GT(">"),
>>> +    GE("=>"),
>>> +    EX(""),
>>> +    LIKE("like");
>>> +
>>> +    private final String op;
>>> +
>>> +    RelationOp(String op) {
>>> +        this.op = op;
>>> +    }
>>> +
>>> +    String getOp() {
>>> +        return op;
>>> +    }
>>> +}
>>>
>>> Added:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ResultIterator.java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**
>>> user/query/ResultIterator.**java?rev=1373392&view=auto<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/query/ResultIterator.java?rev=1373392&view=auto>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ResultIterator.java
>>> (added)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/ResultIterator.java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -0,0 +1,120 @@
>>> +/*
>>> + * 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<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.oak.jcr.**security.user.query;
>>> +
>>> +import java.util.Iterator;
>>> +import java.util.**NoSuchElementException;
>>> +
>>> +/**
>>> + * Implements a query result iterator which only returns a maximum
>>> number
>>> of
>>> + * element from an underlying iterator starting at a given offset.
>>> + *
>>> + * @param<T>  element type of the query results
>>> + *
>>> + * TODO move to query-commons ?
>>> + */
>>> +public class ResultIterator<T>  implements Iterator<T>  {
>>> +
>>> +    public final static int OFFSET_NONE = 0;
>>> +    public final static int MAX_ALL = -1;
>>> +
>>> +    private final Iterator<T>  iterator;
>>> +    private final long offset;
>>> +    private final long max;
>>> +    private int pos;
>>> +    private T next;
>>> +
>>> +    /**
>>> +     * Create a new {@code ResultIterator} with a given offset and
>>> maximum
>>> +     *
>>> +     * @param offset Offset to start iteration at. Must be non negative
>>> +     * @param max Maximum elements this iterator should return.
>>> +     * Set to {@link #MAX_ALL} for all results.
>>> +     * @param iterator the underlying iterator
>>> +     * @throws IllegalArgumentException if offset is negative
>>> +     */
>>> +    private ResultIterator(long offset, long max, Iterator<T>
>>>  iterator) {
>>> +        if (offset<  OFFSET_NONE) {
>>> +            throw new IllegalArgumentException("**Offset must not be
>>> negative");
>>> +        }
>>> +        this.iterator = iterator;
>>> +        this.offset = offset;
>>> +        this.max = max;
>>> +    }
>>> +
>>> +    /**
>>> +     * Returns an iterator respecting the specified {@code offset} and
>>> {@code max}.
>>> +     *
>>> +     * @param offset   offset to start iteration at. Must be non
>>> negative
>>> +     * @param max      maximum elements this iterator should return. Set
>>> to
>>> +     * {@link #MAX_ALL} for all
>>> +     * @param iterator the underlying iterator
>>> +     * @param<T>       element type
>>> +     * @return an iterator which only returns the elements in the given
>>> bounds
>>> +     */
>>> +    public static<T>  Iterator<T>  create(long offset, long max,
>>> Iterator<T>  iterator) {
>>> +        if (offset == OFFSET_NONE&&  max == MAX_ALL) {
>>>
>>> +            // no constraints on offset nor max ->  return the original
>>> iterator.
>>> +            return iterator;
>>> +        } else {
>>> +            return new ResultIterator<T>(offset, max, iterator);
>>> +        }
>>> +    }
>>> +
>>> +    //----------------------------**------------------------------**-<
>>> Iterator>---
>>> +    @Override
>>> +    public boolean hasNext() {
>>> +        if (next == null) {
>>> +            fetchNext();
>>> +        }
>>> +        return next != null;
>>> +    }
>>> +
>>> +    @Override
>>> +    public T next() {
>>> +        if (!hasNext()) {
>>> +            throw new NoSuchElementException();
>>> +        }
>>> +        return consumeNext();
>>> +    }
>>> +
>>> +    @Override
>>> +    public void remove() {
>>> +        throw new UnsupportedOperationException(**);
>>> +    }
>>> +
>>> +    //----------------------------**------------------------------**--<
>>> private>---
>>> +
>>> +    private void fetchNext() {
>>> +        for (; pos<  offset&&  iterator.hasNext(); pos++) {
>>>
>>> +            next = iterator.next();
>>> +        }
>>> +
>>> +        if (pos<  offset || !iterator.hasNext() || max>= 0&&  pos -
>>>
>>> offset + 1>  max) {
>>> +            next = null;
>>> +        } else {
>>> +            next = iterator.next();
>>> +            pos++;
>>> +        }
>>> +    }
>>> +
>>> +    private T consumeNext() {
>>> +        T element = next;
>>> +        next = null;
>>> +        return element;
>>> +    }
>>> +}
>>> \ No newline at end of file
>>>
>>> Added:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/XPathQueryBuilder.**java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**
>>> user/query/XPathQueryBuilder.**java?rev=1373392&view=auto<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/query/XPathQueryBuilder.java?rev=1373392&view=auto>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/XPathQueryBuilder.**java
>>> (added)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/XPathQueryBuilder.**java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -0,0 +1,195 @@
>>> +/*
>>> + * 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<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.oak.jcr.**security.user.query;
>>> +
>>> +import javax.jcr.Value;
>>> +
>>> +import org.apache.jackrabbit.api.**security.user.Authorizable;
>>> +import org.apache.jackrabbit.api.**security.user.QueryBuilder;
>>> +
>>> +public class XPathQueryBuilder implements QueryBuilder<Condition>  {
>>> +
>>> +    private Class<? extends Authorizable>  selector =
>>> Authorizable.class;
>>> +    private String groupName;
>>> +    private boolean declaredMembersOnly;
>>> +    private Condition condition;
>>> +    private String sortProperty;
>>> +    private Direction sortDirection = Direction.ASCENDING;
>>> +    private boolean sortIgnoreCase;
>>> +    private Value bound;
>>> +    private long offset;
>>> +    private long maxCount = -1;
>>> +
>>> +    //----------------------------**---------------------------<
>>> QueryBuilder>---
>>> +    @Override
>>> +    public void setSelector(Class<? extends Authorizable>  selector) {
>>> +        this.selector = selector;
>>> +    }
>>> +
>>> +    @Override
>>> +    public void setScope(String groupName, boolean declaredOnly) {
>>> +        this.groupName = groupName;
>>> +        declaredMembersOnly = declaredOnly;
>>> +    }
>>> +
>>> +    @Override
>>> +    public void setCondition(Condition condition) {
>>> +        this.condition = condition;
>>> +    }
>>> +
>>> +    @Override
>>> +    public void setSortOrder(String propertyName, Direction direction,
>>> boolean ignoreCase) {
>>> +        sortProperty = propertyName;
>>> +        sortDirection = direction;
>>> +        sortIgnoreCase = ignoreCase;
>>> +    }
>>> +
>>> +    @Override
>>> +    public void setSortOrder(String propertyName, Direction direction) {
>>> +        setSortOrder(propertyName, direction, false);
>>> +    }
>>> +
>>> +    @Override
>>> +    public void setLimit(Value bound, long maxCount) {
>>> +        offset = 0;   // Unset any previously set offset
>>> +        this.bound = bound;
>>> +        this.maxCount = maxCount;
>>> +    }
>>> +
>>> +    @Override
>>> +    public void setLimit(long offset, long maxCount) {
>>> +        bound = null; // Unset any previously set bound
>>> +        this.offset = offset;
>>> +        this.maxCount = maxCount;
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition nameMatches(String pattern) {
>>> +        return new Condition.Node(pattern);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition neq(String relPath, Value value) {
>>> +        return new Condition.Property(relPath, RelationOp.NE, value);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition eq(String relPath, Value value) {
>>> +        return new Condition.Property(relPath, RelationOp.EQ, value);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition lt(String relPath, Value value) {
>>> +        return new Condition.Property(relPath, RelationOp.LT, value);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition le(String relPath, Value value) {
>>> +        return new Condition.Property(relPath, RelationOp.LE, value);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition gt(String relPath, Value value) {
>>> +        return new Condition.Property(relPath, RelationOp.GT, value);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition ge(String relPath, Value value) {
>>> +        return new Condition.Property(relPath, RelationOp.GE, value);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition exists(String relPath) {
>>> +        return new Condition.Property(relPath, RelationOp.EX);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition like(String relPath, String pattern) {
>>> +        return new Condition.Property(relPath, RelationOp.LIKE,
>>> pattern);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition contains(String relPath, String searchExpr) {
>>> +        return new Condition.Contains(relPath, searchExpr);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition impersonates(String name) {
>>> +        return new Condition.Impersonation(name);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition not(Condition condition) {
>>> +        return new Condition.Not(condition);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition and(Condition condition1, Condition condition2) {
>>> +        return new Condition.And(condition1, condition2);
>>> +    }
>>> +
>>> +    @Override
>>> +    public Condition or(Condition condition1, Condition condition2) {
>>> +        return new Condition.Or(condition1, condition2);
>>> +    }
>>> +
>>> +    //----------------------------**------------------------------**-<
>>> internal>---
>>> +
>>> +    Condition property(String relPath, RelationOp op, Value value) {
>>> +        return new Condition.Property(relPath, op, value);
>>> +    }
>>> +
>>> +    Class<? extends Authorizable>  getSelector() {
>>> +        return selector;
>>> +    }
>>> +
>>> +    String getGroupName() {
>>> +        return groupName;
>>> +    }
>>> +
>>> +    boolean isDeclaredMembersOnly() {
>>> +        return declaredMembersOnly;
>>> +    }
>>> +
>>> +    Condition getCondition() {
>>> +        return condition;
>>> +    }
>>> +
>>> +    String getSortProperty() {
>>> +        return sortProperty;
>>> +    }
>>> +
>>> +    Direction getSortDirection() {
>>> +        return sortDirection;
>>> +    }
>>> +
>>> +    boolean getSortIgnoreCase() {
>>> +        return sortIgnoreCase;
>>> +    }
>>> +
>>> +    Value getBound() {
>>> +        return bound;
>>> +    }
>>> +
>>> +    long getOffset() {
>>> +        return offset;
>>> +    }
>>> +
>>> +    long getMaxCount() {
>>> +        return maxCount;
>>> +    }
>>> +}
>>> \ No newline at end of file
>>>
>>> Added:
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/**XPathQueryEvaluator.java
>>> URL:
>>> http://svn.apache.org/viewvc/**jackrabbit/oak/trunk/oak-jcr/**
>>> src/main/java/org/apache/**jackrabbit/oak/jcr/security/**user/query/**
>>> XPathQueryEvaluator.java?rev=**1373392&view=auto<http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/query/XPathQueryEvaluator.java?rev=1373392&view=auto>
>>>
>>> ==============================**==============================**
>>> ==================
>>> ---
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/**XPathQueryEvaluator.java
>>> (added)
>>> +++
>>> jackrabbit/oak/trunk/oak-jcr/**src/main/java/org/apache/**
>>> jackrabbit/oak/jcr/security/**user/query/**XPathQueryEvaluator.java
>>> Wed Aug 15 13:24:21 2012
>>> @@ -0,0 +1,340 @@
>>> +/*
>>> + * 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<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.oak.jcr.**security.user.query;
>>> +
>>> +import java.util.Iterator;
>>> +import javax.annotation.Nonnull;
>>> +import javax.jcr.Node;
>>> +import javax.jcr.PropertyType;
>>> +import javax.jcr.RepositoryException;
>>> +import javax.jcr.Value;
>>> +import javax.jcr.query.Query;
>>> +import javax.jcr.query.QueryManager;
>>> +
>>> +import com.google.common.base.**Function;
>>> +import com.google.common.base.**Predicate;
>>> +import com.google.common.base.**Predicates;
>>> +import com.google.common.collect.**Iterators;
>>> +import org.apache.jackrabbit.api.**security.user.Authorizable;
>>> +import org.apache.jackrabbit.api.**security.user.Group;
>>> +import org.apache.jackrabbit.api.**security.user.QueryBuilder;
>>> +import org.apache.jackrabbit.api.**security.user.User;
>>> +import org.apache.jackrabbit.api.**security.user.UserManager;
>>> +import org.apache.jackrabbit.oak.jcr.**security.user.UserManagerImpl;
>>> +import org.apache.jackrabbit.oak.**namepath.NamePathMapper;
>>> +import org.apache.jackrabbit.oak.spi.**security.user.UserConstants;
>>> +import org.apache.jackrabbit.util.**Text;
>>> +import org.slf4j.Logger;
>>> +import org.slf4j.LoggerFactory;
>>> +
>>> +/**
>>> + * This evaluator for {@link
>>> org.apache.jackrabbit.api.**security.user.Query}s use XPath
>>> + * and some minimal client side filtering.
>>> + */
>>> +public class XPathQueryEvaluator implements ConditionVisitor {
>>> +    static final Logger log =
>>> LoggerFactory.getLogger(**XPathQueryEvaluator.class);
>>> +
>>> +    private final XPathQueryBuilder builder;
>>> +    private final UserManager userManager;
>>> +    private final QueryManager queryManager;
>>> +    private final NamePathMapper namePathMapper;
>>> +
>>> +    private final StringBuilder xPath = new StringBuilder();
>>> +
>>> +    public XPathQueryEvaluator(**XPathQueryBuilder builder,
>>> UserManagerImpl
>>> userManager,
>>> +                               QueryManager queryManager, NamePathMapper
>>> namePathMapper) {
>>> +        this.builder = builder;
>>> +        this.userManager = userManager;
>>> +        this.queryManager = queryManager;
>>> +        this.namePathMapper = namePathMapper;
>>> +    }
>>> +
>>> +    public Iterator<Authorizable>  eval() throws RepositoryException {
>>> +        xPath.append("//element(*,")
>>> +                .append(getNtName(builder.**getSelector()))
>>> +                .append(')');
>>> +
>>> +        Value bound = builder.getBound();
>>> +        long offset = builder.getOffset();
>>> +        if (bound != null&&  offset>  0) {
>>>
>>> +            log.warn("Found bound {} and offset {} in limit. Discarding
>>> offset.", bound, offset);
>>> +            offset = 0;
>>> +        }
>>> +
>>> +        Condition condition = builder.getCondition();
>>> +        String sortCol = builder.getSortProperty();
>>> +        QueryBuilder.Direction sortDir = builder.getSortDirection();
>>> +        if (bound != null) {
>>> +            if (sortCol == null) {
>>> +                log.warn("Ignoring bound {} since no sort order is
>>> specified");
>>> +            } else {
>>> +                Condition boundCondition = builder.property(sortCol,
>>> getCollation(sortDir), bound);
>>> +                condition = condition == null
>>> +                        ? boundCondition
>>> +                        : builder.and(condition, boundCondition);
>>> +            }
>>> +        }
>>> +
>>> +        if (condition != null) {
>>> +            xPath.append('[');
>>> +            condition.accept(this);
>>> +            xPath.append(']');
>>> +        }
>>> +
>>> +        if (sortCol != null) {
>>> +            boolean ignoreCase = builder.getSortIgnoreCase();
>>> +            xPath.append(" order by ")
>>> +                    .append(ignoreCase ? "" : "fn:lower-case(")
>>> +                    .append(sortCol)
>>> +                    .append(ignoreCase ? " " : ") ")
>>> +                    .append(sortDir.getDirection()**);
>>> +        }
>>> +
>>> +        Query query = queryManager.createQuery(**xPath.toString(),
>>> Query.XPATH);
>>> +        long maxCount = builder.getMaxCount();
>>> +        if (maxCount == 0) {
>>> +            return Iterators.emptyIterator();
>>> +        }
>>> +
>>> +        // If we are scoped to a group and have a limit, we have to
>>> apply
>>> the limit
>>> +        // here (inefficient!) otherwise we can apply the limit in the
>>> query
>>> +        if (builder.getGroupName() == null) {
>>> +            if (offset>  0) {
>>> +                query.setOffset(offset);
>>> +            }
>>> +            if (maxCount>  0) {
>>> +                query.setLimit(maxCount);
>>> +            }
>>> +            return toAuthorizables(execute(query)**);
>>> +        } else {
>>> +            Iterator<Authorizable>  result =
>>> toAuthorizables(execute(query)**);
>>> +            Iterator<Authorizable>  filtered = filter(result,
>>> builder.getGroupName(), builder.isDeclaredMembersOnly(**));
>>> +            return ResultIterator.create(offset, maxCount, filtered);
>>> +        }
>>> +    }
>>> +
>>> +    //----------------------------**-----------------------<
>>> ConditionVisitor>---
>>> +    @Override
>>> +    public void visit(Condition.Node condition) throws
>>> RepositoryException {
>>> +        xPath.append('(')
>>> +                .append("jcr:like(")
>>> +
>>>   .append(namePathMapper.**getJcrName(UserConstants.REP_**
>>> PRINCIPAL_NAME))
>>> +                .append(",'")
>>> +                .append(condition.getPattern()**)
>>> +                .append("')")
>>> +                .append(" or ")
>>> +                .append("jcr:like(fn:name(.),'**")
>>> +                .append(escape(condition.**getPattern()))
>>> +                .append("')")
>>> +                .append(')');
>>> +    }
>>> +
>>> +    @Override
>>> +    public void visit(Condition.Property condition) throws
>>> RepositoryException {
>>> +        RelationOp relOp = condition.getOp();
>>> +        if (relOp == RelationOp.EX) {
>>> +            xPath.append(condition.**getRelPath());
>>> +        } else if (relOp == RelationOp.LIKE) {
>>> +            xPath.append("jcr:like(")
>>> +                    .append(condition.getRelPath()**)
>>> +                    .append(",'")
>>> +                    .append(condition.getPattern()**)
>>> +                    .append("')");
>>> +        } else {
>>> +            xPath.append(condition.**getRelPath())
>>> +                    .append(condition.getOp().**getOp())
>>> +                    .append(format(condition.**getValue()));
>>> +        }
>>> +    }
>>> +
>>> +    @Override
>>> +    public void visit(Condition.Contains condition) {
>>> +        xPath.append("jcr:contains(")
>>> +                .append(condition.getRelPath()**)
>>> +                .append(",'")
>>> +                .append(condition.**getSearchExpr())
>>> +                .append("')");
>>> +    }
>>> +
>>> +    @Override
>>> +    public void visit(Condition.Impersonation condition) {
>>> +        xPath.append("@rep:**impersonators='")
>>> +                .append(condition.getName())
>>> +                .append('\'');
>>> +    }
>>> +
>>> +    @Override
>>> +    public void visit(Condition.Not condition) throws
>>> RepositoryException
>>> {
>>> +        xPath.append("not(");
>>> +        condition.getCondition().**accept(this);
>>> +        xPath.append(')');
>>> +    }
>>> +
>>> +    @Override
>>> +    public void visit(Condition.And condition) throws
>>> RepositoryException
>>> {
>>> +        int count = 0;
>>> +        for (Condition c : condition) {
>>> +            xPath.append(count++>  0 ? " and " : "");
>>> +            c.accept(this);
>>> +        }
>>> +    }
>>> +
>>> +    @Override
>>> +    public void visit(Condition.Or condition) throws
>>> RepositoryException {
>>> +        int pos = xPath.length();
>>> +
>>> +        int count = 0;
>>> +        for (Condition c : condition) {
>>> +            xPath.append(count++>  0 ? " or " : "");
>>> +            c.accept(this);
>>> +        }
>>> +
>>> +        // Surround or clause with parentheses if it contains more than
>>> one term
>>> +        if (count>  1) {
>>> +            xPath.insert(pos, '(');
>>> +            xPath.append(')');
>>> +        }
>>> +    }
>>> +
>>> +    //----------------------------**------------------------------**--<
>>> private>---
>>> +    /**
>>> +     * Escape {@code string} for matching in jcr escaped node names
>>> +     *
>>> +     * @param string string to escape
>>> +     * @return escaped string
>>> +     */
>>> +    @Nonnull
>>> +    public static String escape(String string) {
>>> +        StringBuilder result = new StringBuilder();
>>> +
>>> +        int k = 0;
>>> +        int j;
>>> +        do {
>>> +            j = string.indexOf('%', k); // split on %
>>> +            if (j<  0) {
>>> +                // jcr escape trail
>>> +
>>>   result.append(Text.**escapeIllegalJcrChars(string.**substring(k)));
>>> +            } else if (j>  0&&  string.charAt(j - 1) == '\\') {
>>>
>>> +                // literal occurrence of % ->  jcr escape
>>> +
>>>   result.append(Text.**escapeIllegalJcrChars(string.**substring(k, j) +
>>> '%'));
>>> +            } else {
>>> +                // wildcard occurrence of % ->  jcr escape all but %
>>> +
>>>   result.append(Text.**escapeIllegalJcrChars(string.**substring(k,
>>> j))).append('%');
>>> +            }
>>> +
>>> +            k = j + 1;
>>> +        } while (j>= 0);
>>> +
>>> +        return result.toString();
>>> +    }
>>> +
>>> +    @Nonnull
>>> +    private String getNtName(Class<? extends Authorizable>  selector) {
>>> +        String ntName;
>>> +        if (User.class.isAssignableFrom(**selector)) {
>>> +            ntName = namePathMapper.getJcrName(**
>>> UserConstants.NT_REP_USER);
>>> +        } else if (Group.class.isAssignableFrom(**selector)) {
>>> +            ntName =
>>> namePathMapper.getJcrName(**UserConstants.NT_REP_GROUP);
>>> +        } else {
>>> +            ntName =
>>> namePathMapper.getJcrName(**UserConstants.NT_REP_**AUTHORIZABLE);
>>> +        }
>>> +        if (ntName == null) {
>>> +            log.warn("Failed to retrieve JCR name for authorizable node
>>> type.");
>>> +            ntName = UserConstants.NT_REP_**AUTHORIZABLE;
>>> +        }
>>> +        return ntName;
>>> +    }
>>> +
>>> +    @Nonnull
>>> +    private static String format(Value value) throws
>>> RepositoryException {
>>> +        switch (value.getType()) {
>>> +            case PropertyType.STRING:
>>> +            case PropertyType.BOOLEAN:
>>> +                return '\'' + value.getString() + '\'';
>>> +
>>> +            case PropertyType.LONG:
>>> +            case PropertyType.DOUBLE:
>>> +                return value.getString();
>>> +
>>> +            case PropertyType.DATE:
>>> +                return "xs:dateTime('" + value.getString() + "')";
>>> +
>>> +            default:
>>> +                throw new RepositoryException("Property of type " +
>>> PropertyType.nameFromValue(**value.getType()) +
>>> +                        " not supported");
>>> +        }
>>> +    }
>>> +
>>> +    @Nonnull
>>> +    private static RelationOp getCollation(QueryBuilder.**Direction
>>> direction) throws RepositoryException {
>>> +        switch (direction) {
>>> +            case ASCENDING:
>>> +                return RelationOp.GT;
>>> +            case DESCENDING:
>>> +                return RelationOp.LT;
>>> +            default:
>>> +                throw new RepositoryException("Unknown sort order " +
>>> direction);
>>> +        }
>>> +    }
>>> +
>>> +    @Nonnull
>>> +    @SuppressWarnings("unchecked")
>>> +    private static Iterator<Node>  execute(Query query) throws
>>> RepositoryException {
>>> +        return query.execute().getNodes();
>>> +    }
>>> +
>>> +    @Nonnull
>>> +    private Iterator<Authorizable>  toAuthorizables(Iterator<Node>
>>>  nodes) {
>>> +        Function<Node, Authorizable>  transformer = new Function<Node,
>>> Authorizable>() {
>>> +            public Authorizable apply(Node node) {
>>> +                try {
>>> +                    return
>>> userManager.**getAuthorizableByPath(node.**getPath());
>>> +                } catch (RepositoryException e) {
>>> +                    log.warn("Cannot create authorizable from node {}",
>>> node);
>>> +                    log.debug(e.getMessage(), e);
>>> +                    return null;
>>> +                }
>>> +            }
>>> +        };
>>> +
>>> +        return Iterators.transform(nodes, transformer);
>>> +    }
>>> +
>>> +    @Nonnull
>>> +    private Iterator<Authorizable>  filter(Iterator<Authorizable>
>>> authorizables,
>>> +                                          String groupName,
>>> +                                          final boolean
>>> declaredMembersOnly) throws RepositoryException {
>>> +        Predicate<Authorizable>  predicate;
>>> +        Authorizable authorizable =
>>> userManager.getAuthorizable(**groupName);
>>> +        if (authorizable == null || !authorizable.isGroup()) {
>>> +            predicate = Predicates.alwaysFalse();
>>> +        } else {
>>> +            final Group group = (Group) authorizable;
>>> +            predicate = new Predicate<Authorizable>() {
>>> +                public boolean apply(Authorizable authorizable) {
>>> +                    try {
>>> +                        return (declaredMembersOnly) ?
>>> group.isDeclaredMember(**authorizable) : group.isMember(authorizable);
>>> +                    } catch (RepositoryException e) {
>>> +                        log.debug("Cannot determine group membership for
>>> {}", authorizable, e.getMessage());
>>> +                        return false;
>>> +                    }
>>> +                }
>>> +            };
>>> +        }
>>> +        return Iterators.filter(**authorizables, predicate);
>>> +    }
>>> +}
>>> \ No newline at end of file
>>>
>>>
>>>
>>>

Mime
  • Unnamed multipart/alternative (inline, None, 0 bytes)
View raw message