org.apache.jena.permissions.impl.SecuredItemImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jena.permissions.impl.SecuredItemImpl.java

Source

/*
 * 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.jena.permissions.impl;

import java.lang.reflect.Proxy;

import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.jena.graph.FrontsTriple;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Triple;
import org.apache.jena.permissions.SecuredItem;
import org.apache.jena.permissions.SecurityEvaluator;
import org.apache.jena.permissions.SecurityEvaluator.Action;
import org.apache.jena.shared.AddDeniedException;
import org.apache.jena.shared.AuthenticationRequiredException;
import org.apache.jena.shared.DeleteDeniedException;
import org.apache.jena.shared.ReadDeniedException;
import org.apache.jena.shared.UpdateDeniedException;
import org.apache.jena.sparql.expr.Expr;
import org.apache.jena.sparql.util.NodeUtils;
import org.apache.jena.util.iterator.ExtendedIterator;
import org.apache.jena.vocabulary.RDF;

/**
 * An abstract implementation of SecuredItem that caches security checks.
 * <p>
 * Security checks are performed at multiple locations. This implementation
 * ensures that during a single operation the specific check is only evaluated
 * once by caching the result.
 * </p>
 * 
 */
public abstract class SecuredItemImpl implements SecuredItem {
    // a key for the secured item.
    private class CacheKey implements Comparable<CacheKey> {
        private final Action action;
        private final Node mNode;
        private final Triple from;
        private final Triple to;
        private Integer hashCode;

        public CacheKey(final Action action, final Node modelNode) {
            this(action, modelNode, null, null);
        }

        public CacheKey(final Action action, final Node modelNode, final Triple to) {
            this(action, modelNode, to, null);
        }

        public CacheKey(final Action action, final Node modelNode, final Triple to, final Triple from) {
            this.action = action;
            this.mNode = modelNode;
            this.to = to;
            this.from = from;
        }

        private int compare(Node n1, Node n2) {
            if (Node.ANY.equals(n1)) {
                if (Node.ANY.equals(n2)) {
                    return Expr.CMP_EQUAL;
                }
                return Expr.CMP_LESS;
            }
            if (Node.ANY.equals(n2)) {
                return Expr.CMP_GREATER;
            }
            return NodeUtils.compareRDFTerms(n1, n2);
        }

        private int compare(Triple t1, Triple t2) {
            if (t1 == null) {
                if (t2 == null) {
                    return Expr.CMP_EQUAL;
                }
                return Expr.CMP_LESS;
            }
            if (t2 == null) {
                return Expr.CMP_GREATER;
            }
            int retval = compare(t1.getSubject(), t2.getSubject());
            if (retval == Expr.CMP_EQUAL) {
                retval = compare(t1.getPredicate(), t2.getPredicate());
            }
            if (retval == Expr.CMP_EQUAL) {
                retval = compare(t1.getObject(), t2.getObject());
            }
            return retval;
        }

        @Override
        public int compareTo(final CacheKey other) {
            int retval = this.action.compareTo(other.action);
            if (retval == Expr.CMP_EQUAL) {
                retval = NodeUtils.compareRDFTerms(this.mNode, other.mNode);
            }
            if (retval == Expr.CMP_EQUAL) {
                retval = compare(this.to, other.to);
            }
            if (retval == Expr.CMP_EQUAL) {
                retval = compare(this.from, other.from);
            }
            return retval;
        }

        @Override
        public boolean equals(final Object o) {
            if (o instanceof CacheKey) {
                return this.compareTo((CacheKey) o) == 0;
            }
            return false;
        }

        @Override
        public int hashCode() {
            if (hashCode == null) {
                hashCode = new HashCodeBuilder().append(action).append(mNode).append(from).append(to).toHashCode();
            }
            return hashCode;
        }
    }

    // the maximum size of the cache
    public static int MAX_CACHE = 100;
    // the cache for this thread.
    public static final ThreadLocal<LRUMap<CacheKey, Boolean>> CACHE = new ThreadLocal<>();
    // the number of times this thread has recursively called the constructor.
    public static final ThreadLocal<Integer> COUNT = new ThreadLocal<>();

    /**
     * May Convert a Jena Node object into the SecurityEvaluator.VARIABLE
     * instance.
     * 
     * @param jenaNode
     *            The Jena node to convert.
     * @return The Node that represents the jenaNode.
     */
    private static Node convert(final Node jenaNode) {

        if (jenaNode.isVariable()) {
            return SecurityEvaluator.VARIABLE;
        }
        return jenaNode;
    }

    /**
     * Convert a Jena Triple into a SecTriple.
     * 
     * @param jenaTriple
     *            The Jena Triple to convert.
     * @return The SecTriple that represents the jenaTriple.
     */
    private static Triple convert(final Triple jenaTriple) {
        if (jenaTriple.getSubject().isVariable() || jenaTriple.getPredicate().isVariable()
                || jenaTriple.getObject().isVariable()) {
            return new Triple(SecuredItemImpl.convert(jenaTriple.getSubject()),
                    SecuredItemImpl.convert(jenaTriple.getPredicate()),
                    SecuredItemImpl.convert(jenaTriple.getObject()));
        }
        return jenaTriple;
    }

    /**
     * Decrement the number of instances of SecuredItem.
     */
    public static void decrementUse() {
        final Integer i = SecuredItemImpl.COUNT.get();
        if (i == null) {
            throw new IllegalStateException("No count on exit");
        }
        if (i < 1) {
            throw new IllegalStateException("No count less than 1");
        }
        if (i == 1) {
            SecuredItemImpl.CACHE.remove();
            SecuredItemImpl.COUNT.remove();
        } else {
            SecuredItemImpl.COUNT.set(i - 1);
        }
    }

    /**
     * Increment the number of instances of SecuredItem.
     */
    public static void incrementUse() {
        final Integer i = SecuredItemImpl.COUNT.get();
        if (i == null) {
            SecuredItemImpl.CACHE.set(new LRUMap<CacheKey, Boolean>(Math.max(SecuredItemImpl.MAX_CACHE, 100)));
            SecuredItemImpl.COUNT.set(1);
        } else {
            SecuredItemImpl.COUNT.set(i + 1);
        }
    }

    // the evaluator we are using
    private final SecurityEvaluator securityEvaluator;

    // the secured node for that names the graph.
    private final Node modelNode;

    // the item holder that we are evaluating.
    private final ItemHolder<?, ?> itemHolder;

    /**
     * Create the SecuredItemImpl.
     * 
     * @param securedItem
     *            The securedItem.
     * @param holder
     *            The Item holder for the securedItem.
     * @throws IllegalArgumentException
     *             if securedItem is null or securedItem.getSecurityEvaluator()
     *             returns null, or the holder is null.
     */
    protected SecuredItemImpl(final SecuredItem securedItem, final ItemHolder<?, ?> holder) {
        if (securedItem == null) {
            throw new IllegalArgumentException("Secured item may not be null");
        }
        if (securedItem.getSecurityEvaluator() == null) {
            throw new IllegalArgumentException("Security evaluator in secured item may not be null");
        }
        if (holder == null) {
            throw new IllegalArgumentException("ItemHolder may not be null");
        }
        this.securityEvaluator = securedItem.getSecurityEvaluator();
        this.modelNode = securedItem.getModelNode();
        this.itemHolder = holder;
    }

    /**
     * Create the SecuredItemImpl.
     * 
     * @param securityEvaluator
     *            the secured evaluator to use.
     * @param modelURI
     *            the URI for the model.
     * @param holder
     *            The holder to use.
     * @throws IllegalArgumentException
     *             if security evaluator is null, modelURI is null or empty, or
     *             holder is null.
     */
    protected SecuredItemImpl(final SecurityEvaluator securityEvaluator, final String modelURI,
            final ItemHolder<?, ?> holder) {
        if (securityEvaluator == null) {
            throw new IllegalArgumentException("Security evaluator may not be null");
        }
        if (StringUtils.isEmpty(modelURI)) {
            throw new IllegalArgumentException("ModelURI may not be empty or null");
        }
        if (holder == null) {
            throw new IllegalArgumentException("ItemHolder may not be null");
        }
        this.securityEvaluator = securityEvaluator;
        this.modelNode = NodeFactory.createURI(modelURI);
        this.itemHolder = holder;
    }

    @Override
    public String toString() throws AuthenticationRequiredException {
        if (canRead()) {
            return itemHolder.getBaseItem().toString();
        }
        return super.toString();
    }

    /**
     * get the cached value.
     * 
     * @param key
     *            The key to look for.
     * @return the value of the security check or <code>null</code> if the value
     *         has not been cached.
     */
    private Boolean cacheGet(final CacheKey key) {
        final LRUMap<CacheKey, Boolean> cache = SecuredItemImpl.CACHE.get();
        return (cache == null) ? null : (Boolean) cache.get(key);
    }

    /**
     * set the cache value.
     * 
     * @param key
     *            The key to set the value for.
     * @param value
     *            The value to set.
     */
    private void cachePut(final CacheKey key, final boolean value) {
        final LRUMap<CacheKey, Boolean> cache = SecuredItemImpl.CACHE.get();
        if (cache != null) {
            cache.put(key, value);
            SecuredItemImpl.CACHE.set(cache);
        }
    }

    @Override
    public boolean canCreate() throws AuthenticationRequiredException {
        final CacheKey key = new CacheKey(Action.Create, modelNode);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Create, modelNode);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canCreate(final Triple triple) throws AuthenticationRequiredException {
        Triple t = convert(triple);
        final CacheKey key = new CacheKey(Action.Create, modelNode, t);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Create, modelNode, t);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canCreate(final FrontsTriple frontsTriple) throws AuthenticationRequiredException {
        return canCreate(frontsTriple.asTriple());
    }

    @Override
    public boolean canDelete() throws AuthenticationRequiredException {
        final CacheKey key = new CacheKey(Action.Delete, modelNode);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Delete, modelNode);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canDelete(final Triple triple) throws AuthenticationRequiredException {
        Triple t = convert(triple);
        final CacheKey key = new CacheKey(Action.Delete, modelNode, t);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Delete, modelNode, t);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canDelete(final FrontsTriple frontsTriple) throws AuthenticationRequiredException {
        return canDelete(frontsTriple.asTriple());
    }

    @Override
    public boolean canRead() throws AuthenticationRequiredException {
        final CacheKey key = new CacheKey(Action.Read, modelNode);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Read, modelNode);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canRead(final Triple triple) throws AuthenticationRequiredException {
        Triple t = convert(triple);
        final CacheKey key = new CacheKey(Action.Read, modelNode, t);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Read, modelNode, t);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canRead(final FrontsTriple frontsTriple) throws AuthenticationRequiredException {
        return canRead(frontsTriple.asTriple());
    }

    @Override
    public boolean canUpdate() throws AuthenticationRequiredException {
        final CacheKey key = new CacheKey(Action.Update, modelNode);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluate(securityEvaluator.getPrincipal(), Action.Update, modelNode);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canUpdate(final Triple f, final Triple t) throws AuthenticationRequiredException {
        Triple from = convert(f);
        Triple to = convert(t);
        final CacheKey key = new CacheKey(Action.Update, modelNode, from, to);
        Boolean retval = cacheGet(key);
        if (retval == null) {
            retval = securityEvaluator.evaluateUpdate(securityEvaluator.getPrincipal(), modelNode, from, to);
            cachePut(key, retval);
        }
        return retval;
    }

    @Override
    public boolean canUpdate(final FrontsTriple from, final FrontsTriple to)
            throws AuthenticationRequiredException {
        return canUpdate(from.asTriple(), to.asTriple());
    }

    /**
     * check that create on the securedModel is allowed,
     * 
     * @throws AddDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkCreate() throws AddDeniedException, AuthenticationRequiredException {
        if (!canCreate()) {
            throw new AddDeniedException(SecuredItem.Util.modelPermissionMsg(modelNode));
        }
    }

    /**
     * check that the triple can be created in the securedModel.,
     * 
     * @throws AddDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkCreate(final Triple t) throws AddDeniedException, AuthenticationRequiredException {
        if (!canCreate(t)) {
            throw new AddDeniedException(SecuredItem.Util.triplePermissionMsg(modelNode), t);
        }
    }

    /**
     * check that the statement can be created.
     * 
     * @param s
     *            The statement.
     * @throws AddDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkCreate(final FrontsTriple frontsTriple)
            throws AddDeniedException, AuthenticationRequiredException {
        checkCreate(frontsTriple.asTriple());
    }

    /**
     * Check that a triple can be reified.
     * 
     * @param uri
     *            The URI for the reification subject. May be null.
     * @param front
     *            the frontstriple that is to be reified.
     * @throws AddDeniedException
     *             on failure to add triple
     * @throws UpdateDeniedException
     *             if the updates of the graph are not allowed.
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkCreateReified(final String uri, final FrontsTriple front)
            throws AddDeniedException, UpdateDeniedException, AuthenticationRequiredException {
        checkUpdate();
        Triple t = front.asTriple();
        final Node n = uri == null ? SecurityEvaluator.FUTURE : NodeFactory.createURI(uri);
        checkCreate(new Triple(n, RDF.subject.asNode(), t.getSubject()));
        checkCreate(new Triple(n, RDF.predicate.asNode(), t.getPredicate()));
        checkCreate(new Triple(n, RDF.object.asNode(), t.getObject()));
    }

    /**
     * Check that all the triples can be created.
     * 
     * @param FrontsTripleIter
     *            an iterator of FrontsTriple objects.
     * @throws AddDeniedException
     *             if a triple can not be added.
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkCreateFrontsTriples(final ExtendedIterator<? extends FrontsTriple> FrontsTripleIter)
            throws AddDeniedException, AuthenticationRequiredException {
        if (!canCreate(Triple.ANY)) {
            try {
                while (FrontsTripleIter.hasNext()) {
                    checkCreate(FrontsTripleIter.next());
                }
            } finally {
                FrontsTripleIter.close();
            }
        }
    }

    /**
     * Check that all the triples can be created.
     * 
     * @param triples
     *            an iterator of triples.
     * @throws AddDeniedException
     *             if a triple can not be added.
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkCreateTriples(final ExtendedIterator<Triple> triples)
            throws AddDeniedException, AuthenticationRequiredException {
        if (!canCreate(Triple.ANY)) {
            try {
                while (triples.hasNext()) {
                    checkCreate(triples.next());
                }
            } finally {
                triples.close();
            }
        }
    }

    /**
     * check that delete on the securedModel is allowed,
     * 
     * @throws DeleteDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkDelete() throws DeleteDeniedException, AuthenticationRequiredException {
        if (!canDelete()) {
            throw new DeleteDeniedException(SecuredItem.Util.modelPermissionMsg(modelNode));
        }
    }

    /**
     * check that the triple can be deleted in the securedModel.,
     * 
     * @param triple
     *            The triple to check.
     * @throws DeleteDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkDelete(final Triple triple) throws DeleteDeniedException, AuthenticationRequiredException {
        if (!canDelete(triple)) {
            throw new DeleteDeniedException(SecuredItem.Util.triplePermissionMsg(modelNode), triple);
        }
    }

    /**
     * check that the triple can be deleted in the securedModel.,
     * 
     * @param frontsTriple
     *            An object fronting the triple to check.
     * @throws DeleteDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkDelete(final FrontsTriple frontsTriple)
            throws DeleteDeniedException, AuthenticationRequiredException {
        checkDelete(frontsTriple.asTriple());
    }

    /**
     * check that the triples can be deleted in the securedModel.,
     * 
     * @param frontsTripleIter
     *            An iterator of objects fronting triples to check.
     * @throws DeleteDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkDeleteFrontsTriples(final ExtendedIterator<? extends FrontsTriple> frontsTriplesIter)
            throws DeleteDeniedException, AuthenticationRequiredException {
        if (!canDelete(Triple.ANY)) {
            try {
                while (frontsTriplesIter.hasNext()) {
                    checkDelete(frontsTriplesIter.next());
                }
            } finally {
                frontsTriplesIter.close();
            }
        }
    }

    /**
     * check that the triples can be deleted in the securedModel.,
     * 
     * @param triples
     *            An iterator of triples to check.
     * @throws DeleteDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkDeleteTriples(final ExtendedIterator<Triple> triples)
            throws DeleteDeniedException, AuthenticationRequiredException {
        if (!canDelete(Triple.ANY)) {
            try {
                while (triples.hasNext()) {
                    checkDelete(triples.next());
                }
            } finally {
                triples.close();
            }
        }
    }

    /**
     * check that read on the securedModel is allowed,
     * 
     * @throws ReadDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkRead() throws ReadDeniedException, AuthenticationRequiredException {
        if (!canRead()) {
            throw new ReadDeniedException(SecuredItem.Util.modelPermissionMsg(modelNode));
        }
    }

    /**
     * check that the triple can be read in the securedModel.,
     * 
     * @param triple
     *            The triple to check.
     * @throws ReadDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkRead(final Triple triple) throws ReadDeniedException, AuthenticationRequiredException {
        if (!canRead(triple)) {
            throw new ReadDeniedException(SecuredItem.Util.triplePermissionMsg(modelNode), triple);
        }
    }

    /**
     * check that the triple can be read in the securedModel.,
     * 
     * @param frontsTriple
     *            The object fronting the triple to check.
     * @throws ReadDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkRead(final FrontsTriple frontsTriple)
            throws ReadDeniedException, AuthenticationRequiredException {
        checkRead(frontsTriple.asTriple());
    }

    /**
     * check that the triple can be read in the securedModel.,
     * 
     * @param frontsTripleIter
     *            The iterator of fronts triple objects to check.
     * @throws ReadDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkReadFrontsTriples(final ExtendedIterator<FrontsTriple> frontsTripleIter)
            throws ReadDeniedException, AuthenticationRequiredException {
        try {
            while (frontsTripleIter.hasNext()) {
                checkRead(frontsTripleIter.next());
            }
        } finally {
            frontsTripleIter.close();
        }
    }

    /**
     * check that the triple can be read in the securedModel.,
     * 
     * @param triples
     *            The iterator of triples to check.
     * @throws ReadDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkReadTriples(final ExtendedIterator<Triple> triples)
            throws ReadDeniedException, AuthenticationRequiredException {
        try {
            while (triples.hasNext()) {
                checkRead(triples.next());
            }
        } finally {
            triples.close();
        }
    }

    /**
     * check that update on the securedModel is allowed,
     * 
     * @throws UpdateDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkUpdate() throws UpdateDeniedException, AuthenticationRequiredException {
        if (!canUpdate()) {
            throw new UpdateDeniedException(SecuredItem.Util.modelPermissionMsg(modelNode));
        }
    }

    /**
     * check that the triple can be updated in the securedModel.,
     * 
     * @param from
     *            the starting triple
     * @param to
     *            the final triple.
     * @throws UpdateDeniedException
     *             on failure
     * @throws AuthenticationRequiredException
     *             if user is not authenticated and is required to be.
     */
    protected void checkUpdate(final Triple from, final Triple to)
            throws UpdateDeniedException, AuthenticationRequiredException {
        if (!canUpdate(from, to)) {
            throw new UpdateDeniedException(
                    String.format("%s: %s to %s", SecuredItem.Util.modelPermissionMsg(modelNode), from, to));
        }
    }

    @Override
    public boolean equals(final Object o) {
        if (Proxy.isProxyClass(o.getClass())) {
            return o.equals(itemHolder.getSecuredItem());
        } else {
            if (o instanceof SecuredItemImpl) {
                return itemHolder.getBaseItem().equals(((SecuredItemImpl) o).getBaseItem());
            }
            return false;
        }
    }

    @Override
    public int hashCode() {
        return itemHolder.getBaseItem().hashCode();
    }

    @Override
    public Object getBaseItem() {
        return itemHolder.getBaseItem();
    }

    @Override
    public String getModelIRI() {
        return modelNode.getURI();
    }

    /**
     * get the name of the model.
     */
    @Override
    public Node getModelNode() {
        return modelNode;
    }

    @Override
    public SecurityEvaluator getSecurityEvaluator() {
        return securityEvaluator;
    }

    @Override
    public boolean isEquivalent(final SecuredItem securedItem) {
        return SecuredItem.Util.isEquivalent(this, securedItem);
    }
}