/* * Copyright (c) 2007, 2010, James Leigh All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the openrdf.org nor the names of its contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ package net.enilink.composition.properties.komma; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import net.enilink.commons.iterator.ConvertingIterator; import net.enilink.commons.iterator.Filter; import net.enilink.commons.iterator.IExtendedIterator; import net.enilink.commons.iterator.NiceIterator; import net.enilink.commons.iterator.WrappedIterator; import net.enilink.composition.properties.Filterable; import net.enilink.composition.properties.PropertySet; import net.enilink.composition.properties.exceptions.PropertyException; import net.enilink.composition.properties.traits.Mergeable; import net.enilink.komma.core.IEntity; import net.enilink.komma.core.IEntityManager; import net.enilink.komma.core.ILiteral; import net.enilink.komma.core.IQuery; import net.enilink.komma.core.IReference; import net.enilink.komma.core.IReferenceable; import net.enilink.komma.core.ITransaction; import net.enilink.komma.core.KommaException; import net.enilink.komma.core.Statement; import net.enilink.komma.core.URI; import com.google.inject.Inject; /** * A set for a given subject and predicate. * * @param <E> */ public class KommaPropertySet<E> implements PropertySet<E>, Set<E>, Filterable<E> { private static final int CACHE_LIMIT = 10; protected static final String QUERY = "SELECT DISTINCT ?o WHERE { ?s ?p ?o }"; protected final IReference bean; private volatile List<E> cache; @Inject protected IEntityManager manager; protected IReference property; protected Class<E> valueType; protected URI rdfValueType; public KommaPropertySet(IReference bean, IReference property) { this(bean, property, null, null); } public KommaPropertySet(IReference bean, IReference property, Class<E> valueType, URI rdfValueType) { assert bean != null; assert property != null; this.bean = bean; this.property = property; this.valueType = valueType; this.rdfValueType = rdfValueType; } /** * This method always returns <code>true</code> * * @return <code>true</code> */ public boolean add(E o) { refresh(); try { manager.add(new Statement(bean, property, convertInstance(o))); } catch (KommaException e) { throw new PropertyException(e); } refresh(bean); refresh(o); return true; } public boolean addAll(Collection<? extends E> c) { refresh(); boolean modified = false; ITransaction transaction = manager.getTransaction(); try { boolean active = transaction.isActive(); if (!active) { transaction.begin(); } try { for (E o : c) { if (add(o)) { modified = true; } } if (!active) { transaction.commit(); } } finally { if (!active && transaction.isActive()) { transaction.rollback(); } } } catch (KommaException e) { throw new PropertyException(e); } refresh(bean); return modified; } public void clear() { manager.remove(new Statement(bean, property, null)); refreshCache(); refresh(); refresh(bean); } protected boolean containsWithoutCache(Object o) { try { return manager.createQuery("ASK { ?s ?p ?o }") .setParameter("s", bean).setParameter("p", property) .setParameter("o", convertInstance(o)).getBooleanResult(); } catch (KommaException e) { throw new PropertyException(e); } } public boolean contains(Object o) { if (!(o instanceof ILiteral)) { // raw literals are handled different List<E> cache = getCache(); if (isCacheComplete(cache)) { return cache.contains(o); } if (cache != null && cache.contains(o)) { return true; } } return containsWithoutCache(o); } public boolean containsAll(Collection<?> c) { List<E> cache = getCache(); if (cache != null) { boolean allInCache = true; for (Object e : c) { if (e instanceof ILiteral) { // raw literals are handled different if (!containsWithoutCache(e)) { return false; } } else if (!cache.contains(e)) { allInCache = false; break; } } if (allInCache || isCacheComplete(cache)) { return allInCache; } } for (Object element : c) { if (!containsWithoutCache(element)) { return false; } } return true; } protected Collection<Class<?>> findConcepts(URI rdfType) { Collection<Class<?>> roles = manager.rolesForType(rdfType); return WrappedIterator.create(roles.iterator()) .filterKeep(new Filter<Class<?>>() { @Override public boolean accept(Class<?> o) { return o.isInterface(); } }).toList(); } protected Object convertInstance(Object instance) { // handle the explicit rdf:type set with the @Type annotation if (rdfValueType != null && !(instance instanceof IReference || instance instanceof IReferenceable)) { Collection<Class<?>> roles = findConcepts(rdfValueType); if (!roles.isEmpty()) { // test if instance already has the required roles boolean hasValidType = true; for (Class<?> role : roles) { if (!role.isAssignableFrom(instance.getClass())) { hasValidType = false; break; } } if (!hasValidType) { // create a new instance with correct rdf:type IEntity newEntity = (IEntity) manager.create(rdfValueType); if (newEntity instanceof Mergeable) { try { ((Mergeable) newEntity).merge(instance); } catch (Exception e) { throw new KommaException(e); } } return newEntity; } } } return instance; } @SuppressWarnings("unchecked") protected IExtendedIterator<E> evaluateQueryForTypes(IQuery<?> query) { // handle the explicit rdf:type set from the @Type annotation if (rdfValueType != null) { Collection<Class<?>> roles = findConcepts(rdfValueType); if (!roles.isEmpty()) { Iterator<Class<?>> it = roles.iterator(); Class<?> role1 = it.next(); it.remove(); return (IExtendedIterator<E>) query.evaluate(role1, roles.toArray(new Class<?>[0])); } } if (valueType != null) { return query.evaluate(valueType); } return (IExtendedIterator<E>) query.evaluate(); } protected IQuery<?> createElementsQuery(String query, String filterPattern, int limit) { boolean useFilter = filterPattern != null && !filterPattern.isEmpty(); if (useFilter || limit != Integer.MAX_VALUE) { StringBuilder querySb = new StringBuilder(query); if (useFilter) { querySb.insert(query.lastIndexOf('}'), " FILTER regex(str(?o), ?filter, \"i\")"); } if (limit != Integer.MAX_VALUE) { querySb.append(" LIMIT " + limit); } query = querySb.toString(); } IQuery<?> result = manager.createQuery(query).setParameter("s", bean) .setParameter("p", property); if (useFilter) { result.setParameter("filter", ".*" + filterPattern + ".*"); } return result; } protected IExtendedIterator<E> createElementsIterator() { return createElementsIterator(null, Integer.MAX_VALUE); } protected IExtendedIterator<E> createElementsIterator( final String filterPattern, final int limit) { IQuery<?> query = createElementsQuery(QUERY, filterPattern, limit); return new ConvertingIterator<E, E>(evaluateQueryForTypes(query)) { private List<E> list = filterPattern == null && limit == Integer.MAX_VALUE ? new ArrayList<E>(Math.min( CACHE_LIMIT, getCacheLimit())) : null; private E current; @Override public void close() { if (list != null && (!hasNext() || list.size() == getCacheLimit())) { setCache(list); } try { super.close(); } catch (KommaException e) { throw new PropertyException(e); } } protected E convert(E value) { if (list != null && list.size() < getCacheLimit()) { list.add(value); } return value; } @Override public boolean hasNext() { try { return super.hasNext(); } catch (KommaException e) { throw new PropertyException(e); } } @Override public E next() { try { return current = super.next(); } catch (KommaException e) { throw new PropertyException(e); } } @Override public void remove() { if (current == null) { throw new NoSuchElementException(); } KommaPropertySet.this.remove(current); } }; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; KommaPropertySet<?> other = (KommaPropertySet<?>) obj; if (bean == null) { if (other.bean != null) return false; } else if (!bean.equals(other.bean)) return false; if (manager == null) { if (other.manager != null) return false; } else if (!manager.equals(other.manager)) return false; if (property == null) { if (other.property != null) return false; } else if (!property.equals(other.property)) return false; return true; } public Set<E> getAll() { return this; } protected final List<E> getCache() { return cache; } protected int getCacheLimit() { return CACHE_LIMIT; } public Class<E> getElementType() { return valueType; } @SuppressWarnings("unchecked") public E getSingle() { List<E> cache = getCache(); if (cache != null) { if (cache.isEmpty()) { if (valueType != null && valueType.isPrimitive()) { return (E) ConversionUtil.convertValue(valueType, 0, null); } return null; } else { return cache.get(0); } } try { IExtendedIterator<E> iter = createElementsIterator(); try { if (iter.hasNext()) { return iter.next(); } return null; } finally { iter.close(); } } catch (KommaException e) { throw new PropertyException(e); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((bean == null) ? 0 : bean.hashCode()); result = prime * result + ((manager == null) ? 0 : manager.hashCode()); result = prime * result + ((property == null) ? 0 : property.hashCode()); return result; } @Override public void init(Collection<? extends E> values) { if (getCache() == null) { setCache(new ArrayList<E>(values)); } } private boolean isCacheComplete(List<E> cache) { return cache != null && cache.size() < getCacheLimit(); } public boolean isEmpty() { List<E> cache = getCache(); if (cache != null) { return cache.isEmpty(); } IExtendedIterator<E> iter = createElementsIterator(); try { return !iter.hasNext(); } finally { iter.close(); } } public IExtendedIterator<E> iterator() { List<E> cache = getCache(); if (isCacheComplete(cache)) { final Iterator<E> iter = cache.iterator(); return new NiceIterator<E>() { private E e; public boolean hasNext() { return iter.hasNext(); } public E next() { return e = iter.next(); } public void remove() { KommaPropertySet.this.remove(e); } }; } try { return createElementsIterator(); } catch (KommaException e) { throw new PropertyException(e); } } public void refresh() { setCache(null); } protected void refresh(Object o) { manager.refresh(o); } protected void refreshCache() { List<E> cache = getCache(); if (cache != null) { for (E e : cache) { refresh(e); } } } /** * This method always returns <code>true</code> * * @return <code>true</code> */ public boolean remove(Object o) { refresh(); manager.remove(new Statement(bean, property, convertInstance(o))); refresh(o); refresh(bean); return true; } public boolean removeAll(Collection<?> c) { boolean modified = false; try { ITransaction transaction = manager.getTransaction(); boolean active = transaction.isActive(); if (!active) { transaction.begin(); } try { for (Object o : c) { if (remove(o)) { modified = true; } } if (!active) { transaction.commit(); } } finally { if (!active && transaction.isActive()) { transaction.rollback(); } } } catch (KommaException e) { throw new PropertyException(e); } refreshCache(); refresh(bean); return modified; } public boolean retainAll(Collection<?> c) { refresh(); boolean modified = false; try { ITransaction transaction = manager.getTransaction(); boolean active = transaction.isActive(); if (!active) { transaction.begin(); } try { IExtendedIterator<E> e = createElementsIterator(); try { while (e.hasNext()) { if (!c.contains(e.next())) { remove(e); modified = true; } } } finally { e.close(); } if (!active) { transaction.commit(); } } finally { if (!active && transaction.isActive()) { transaction.rollback(); } } } catch (KommaException e) { throw new PropertyException(e); } refreshCache(); refresh(bean); return modified; } public void setAll(Set<E> set) { if (this == set) { return; } if (set == null) { clear(); return; } Set<E> c = new HashSet<E>(set); ITransaction transaction = manager.getTransaction(); try { boolean active = transaction.isActive(); if (!active) { transaction.begin(); } try { List<E> cache = getCache(); if (cache == null || !cache.isEmpty()) { clear(); } addAll(c); if (!active) { transaction.commit(); } } finally { if (!active && transaction.isActive()) { transaction.rollback(); } } } catch (KommaException e) { throw new PropertyException(e); } refreshCache(); } protected void setCache(List<E> cache) { this.cache = cache; } public void setSingle(E o) { if (o == null) { clear(); } else { ITransaction transaction = manager.getTransaction(); try { boolean active = transaction.isActive(); if (!active) { transaction.begin(); } try { List<E> cache = getCache(); if (cache == null || !cache.isEmpty()) { clear(); } add(o); if (!active) { transaction.commit(); } } finally { if (!active && transaction.isActive()) { transaction.rollback(); } } } catch (KommaException e) { throw new PropertyException(e); } } } public int size() { List<E> cache = getCache(); if (isCacheComplete(cache)) { return cache.size(); } IExtendedIterator<IReference> values = manager.createQuery(QUERY) .setParameter("s", bean).setParameter("p", property) .evaluateRestricted(IReference.class); try { int size; for (size = 0; values.hasNext(); size++) { values.next(); } return size; } finally { values.close(); } } public Object[] toArray() { List<E> cache = getCache(); if (isCacheComplete(cache)) { return cache.toArray(); } IExtendedIterator<E> iter = createElementsIterator(); try { return iter.toList().toArray(); } finally { iter.close(); } } public <T> T[] toArray(T[] a) { List<E> cache = getCache(); if (isCacheComplete(cache)) { return cache.toArray(a); } IExtendedIterator<E> iter = createElementsIterator(); try { return iter.toList().toArray(a); } finally { iter.close(); } } @Override public String toString() { List<E> cache = getCache(); StringBuilder sb = new StringBuilder(); Iterator<E> iter = isCacheComplete(cache) ? cache.iterator() : createElementsIterator(); try { if (iter.hasNext()) { sb.append(iter.next().toString()); } while (iter.hasNext()) { sb.append(", "); sb.append(iter.next()); } } finally { if (iter instanceof AutoCloseable) { try { ((AutoCloseable) iter).close(); } catch (Exception e) { // ignore } } } return sb.toString(); } @Override public Iterator<E> filter(String pattern, int limit) { return createElementsIterator(pattern, limit); } @Override public Iterator<E> filter(String pattern) { return filter(pattern, Integer.MAX_VALUE); } }