blob: 20348174e079865cb74b89695969e0a28e1b296b [file] [log] [blame]
/*
* 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.ace.client.repository.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.ace.client.repository.Associatable;
import org.apache.ace.client.repository.Association;
import org.apache.ace.client.repository.RepositoryObject;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.event.Event;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
/**
* A basic implementation of the Association interface. Implements 'common' behavior for all associations.
*
* The association allows up to m-to-n associations. Each end of the association gets
* a filter to match objects, possibly a cardinality (if no cardinality is specified,
* 1 is assumed), and, if there can be more matches to the filter than the given cardinality,
* a comparator should be provided in the constructor.
*
* @param <L> The type of the RepositoryObject on the left side of this association
* @param <R> The type of the RepositoryObject on the right side of this association
* @param <T> The non-generic Association interface this object should use.
*/
public class AssociationImpl<L extends RepositoryObject, R extends RepositoryObject, T extends Association<L, R>> extends RepositoryObjectImpl<T> implements Association<L, R> {
/* These lists are volatile, since we use copy-on-write semantics for
* updating them.
*/
private volatile List<L> m_left = new ArrayList<>();
private volatile List<R> m_right = new ArrayList<>();
private final Object m_lock = new Object();
private final Filter m_filterLeft;
private final Filter m_filterRight;
private final ObjectRepositoryImpl<?, L> m_leftRepository;
private final ObjectRepositoryImpl<?, R> m_rightRepository;
private final Class<L> m_leftClass;
private final Class<R> m_rightClass;
/**
* Constructor intended for deserialization. For most parameters, see below.
* @param reader a stream reader which contains an XML representation of this object's contents.
*/
public AssociationImpl(HierarchicalStreamReader reader, ChangeNotifier notifier, Class<L> leftClass, Class<R> rightClass, Comparator<L> leftComparator, Comparator<R> rightComparator, ObjectRepositoryImpl<?, L> leftRepository, ObjectRepositoryImpl<?, R> rightRepository, String xmlNode) throws InvalidSyntaxException {
this(readMap(reader), notifier, leftClass, rightClass, leftRepository, rightRepository, xmlNode);
}
/**
* Basic constructor for AssociationImpl.
* @param attributes A map of attributes. This should at least contain <code>Association.LEFT_ENDPOINT</code> and <code>Association.RIGHT_ENDPOINT</code>,
* and optionally <code>Association.LEFT_CARDINALITY</code> and <code>Association.RIGHT_CARDINALITY</code>.
* @param notifier An instance of the event admin
* @param leftClass The class on the left side of this association.
* @param rightClass The class on the right side of this association.
* @param leftRepository The repository which holds object of <code>leftClass</code>.
* @param rightRepository The repository which holds object of <code>rightClass</code>.
* @param xmlNode The tag by which this object is known in the XML representation.
* @throws InvalidSyntaxException Thrown when the attributes contain an invalidly constructed filter string.
*/
public AssociationImpl(Map<String, String> attributes, ChangeNotifier notifier, Class<L> leftClass, Class<R> rightClass, ObjectRepositoryImpl<?, L> leftRepository, ObjectRepositoryImpl<?, R> rightRepository, String xmlNode) throws InvalidSyntaxException {
super(attributes, notifier, xmlNode);
if ((getAttribute(LEFT_CARDINALITY) != null) && (Integer.parseInt(getAttribute(LEFT_CARDINALITY)) < 1)) {
throw new IllegalArgumentException("The left cardinality should be 1 or greater.");
}
if ((getAttribute(RIGHT_CARDINALITY) != null) && (Integer.parseInt(getAttribute(RIGHT_CARDINALITY)) < 1)) {
throw new IllegalArgumentException("The right cardinality should be 1 or greater.");
}
m_leftClass = leftClass;
m_rightClass = rightClass;
m_leftRepository = leftRepository;
m_rightRepository = rightRepository;
m_filterLeft = m_leftRepository.createFilter(getAttribute(Association.LEFT_ENDPOINT));
m_filterRight = m_rightRepository.createFilter(getAttribute(Association.RIGHT_ENDPOINT));
locateLeftEndpoint(false);
locateRightEndpoint(false);
}
public AssociationImpl(Map<String, String> attributes, Map<String, String> tags, ChangeNotifier notifier, Class<L> leftClass, Class<R> rightClass, ObjectRepositoryImpl<?, L> leftRepository, ObjectRepositoryImpl<?, R> rightRepository, String xmlNode) throws InvalidSyntaxException {
super(attributes, tags, notifier, xmlNode);
if ((getAttribute(LEFT_CARDINALITY) != null) && (Integer.parseInt(getAttribute(LEFT_CARDINALITY)) < 1)) {
throw new IllegalArgumentException("The left cardinality should be 1 or greater.");
}
if ((getAttribute(RIGHT_CARDINALITY) != null) && (Integer.parseInt(getAttribute(RIGHT_CARDINALITY)) < 1)) {
throw new IllegalArgumentException("The right cardinality should be 1 or greater.");
}
m_leftClass = leftClass;
m_rightClass = rightClass;
m_leftRepository = leftRepository;
m_rightRepository = rightRepository;
m_filterLeft = m_leftRepository.createFilter(getAttribute(Association.LEFT_ENDPOINT));
m_filterRight = m_rightRepository.createFilter(getAttribute(Association.RIGHT_ENDPOINT));
locateLeftEndpoint(false);
locateRightEndpoint(false);
}
public List<Associatable> getTo(Associatable from) {
if (m_left.contains(from)) {
return new ArrayList<Associatable>(m_right);
}
if (m_right.contains(from)) {
return new ArrayList<Associatable>(m_left);
}
return null;
}
public List<L> getLeft() {
return new ArrayList<>(m_left);
}
public List<R> getRight() {
return new ArrayList<>(m_right);
}
public void remove() {
for (L l : m_left) {
l.remove(this, m_rightClass);
}
for (R r : m_right) {
r.remove(this, m_leftClass);
}
}
/**
* Locates the most suited endpoint of one side of this association. If the corresponding filter
* matches multiple objects, the <code>comparator</code> will be used to find the most suited one.
* The association will register itself with a new endpoint, and remove itself from the old one.
* @param <TYPE> (only used for type matching).
* @param objectRepositoryImpl The repository where the endpoint should come from.
* @param filter A filter string, used to get candidate-endpoints from <code>objectRepositoryImpl</code>.
* @param endpoint The current endpoint.
* @param comparator A comparator, used when there are multiple potential endpoints.
* @param clazz The class of the 'other side' of this association.
* @return The most suited endpoint; this could be equal to <code>endpoint</code>.
*/
private <TYPE extends RepositoryObject> List<TYPE> locateEndpoint(ObjectRepositoryImpl<?, TYPE> objectRepositoryImpl, Filter filter, List<TYPE> endpoints, int cardinality, Class<? extends RepositoryObject> clazz, boolean notify) {
List<TYPE> candidates = objectRepositoryImpl.get(filter);
if (candidates.size() > cardinality) {
Comparator<TYPE> comparator = candidates.get(0).getComparator();
if (comparator != null) {
Collections.sort(candidates, comparator);
}
else {
throw new NullPointerException("Filter '" + filter.toString() + "' in '" + this + "' has resulted in multiple candidates, so the RepositoryObject descendents should have provide a comparator, which they do not.");
}
}
List<TYPE> oldEndpoints = new ArrayList<>(endpoints);
List<TYPE> newEndpoints = new ArrayList<>();
for (int i = 0; (i < cardinality) && !candidates.isEmpty(); i++) {
TYPE current = candidates.remove(0);
newEndpoints.add(current);
if (!oldEndpoints.remove(current)) {
current.add(this, clazz);
}
}
for (TYPE e : oldEndpoints) {
e.remove(this, clazz);
}
return newEndpoints;
}
/**
* Locates the left endpoint by using the generic locateEndpoint and notifies
* listeners of changes, if any.
* @param notify Indicates whether notifications should be sent out.
*/
private void locateLeftEndpoint(boolean notify) {
synchronized (m_lock) {
List<L> newEndpoints = locateEndpoint(m_leftRepository, m_filterLeft, m_left, (getAttribute(LEFT_CARDINALITY) == null ? 1 : Integer.parseInt(getAttribute(LEFT_CARDINALITY))), m_rightClass, notify);
if (!newEndpoints.equals(m_left)) {
if (notify) {
List<L> oldEndpoints = new ArrayList<>(m_left);
m_left = new ArrayList<>(newEndpoints);
Properties props = new Properties();
props.put(EVENT_OLD, oldEndpoints);
props.put(EVENT_NEW, newEndpoints);
notifyChanged(props);
}
else {
m_left = newEndpoints;
}
}
}
}
/**
* Locates the right endpoint by using the generic locateEndpoint and notifies
* listeners of changes, if any.
* @param notify Indicates whether notifications should be sent out.
*/
private void locateRightEndpoint(boolean notify) {
synchronized (m_lock) {
List<R> newEndpoints = locateEndpoint(m_rightRepository, m_filterRight, m_right, (getAttribute(RIGHT_CARDINALITY) == null ? 1 : Integer.parseInt(getAttribute(RIGHT_CARDINALITY))), m_leftClass, notify);
if (!newEndpoints.equals(m_right)) {
if (notify) {
List<R> oldEndpoints = new ArrayList<>(m_right);
m_right = new ArrayList<>(newEndpoints);
Properties props = new Properties();
props.put(EVENT_OLD, oldEndpoints);
props.put(EVENT_NEW, newEndpoints);
notifyChanged(props);
}
else {
m_right = newEndpoints;
}
}
}
}
public boolean isSatisfied() {
return (!m_left.isEmpty()) && (!m_right.isEmpty());
}
@Override
public void handleEvent(Event event) {
// We get a topic which ends in '/*', but the event contains a specialized topic.
// for now, we chop of the star, and check whether the topic starts with that.
RepositoryObject entity = (RepositoryObject) event.getProperty(RepositoryObject.EVENT_ENTITY);
if ((event.getTopic().endsWith("ADDED")) || event.getTopic().endsWith("REMOVED")) {
if (m_leftClass.isInstance(entity) && m_filterLeft.matchCase(entity.getDictionary())) {
locateLeftEndpoint(true);
}
if (m_rightClass.isInstance(entity) && m_filterRight.matchCase(entity.getDictionary())) {
locateRightEndpoint(true);
}
}
}
}