org.protempa.dest.table.Link Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of protempa-framework Show documentation
Show all versions of protempa-framework Show documentation
Protempa Framework is the core of Protempa.
/*
* #%L
* Protempa Framework
* %%
* Copyright (C) 2012 - 2013 Emory University
* %%
* Licensed 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.
* #L%
*/
package org.protempa.dest.table;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.protempa.KnowledgeSource;
import org.protempa.KnowledgeSourceCache;
import org.protempa.KnowledgeSourceReadException;
import org.protempa.PropositionDefinition;
import org.protempa.ProtempaUtil;
import org.protempa.proposition.Proposition;
import org.protempa.proposition.UniqueId;
import org.protempa.proposition.value.Value;
import org.protempa.proposition.value.ValueComparator;
import org.protempa.dest.QueryResultsHandlerValidationFailedException;
/**
* Representation of links between propositions in the PROTEMPA virtual data
* model. Links may be derivations or references. This class represents a link
* as being from one proposition to a collection of propositions. The
* collection of propositions may be filtered by proposition id, the values of
* its properties, and then by index (according to some specified ordering of
* the propositions).
*
* @author Andrew Post
*/
public abstract class Link {
private static final PropertyConstraint[] EMPTY_PROPERTY_CONSTRAINT_ARR =
new PropertyConstraint[0];
private final Set propIdsAsSet;
private final PropertyConstraint[] constraints;
private final Comparator comparator;
private final int fromIndex;
private final int toIndex;
/**
* Specifies a link with proposition ids, constraints,
* an index range for selecting from the list of propositions that match
* the proposition ids and constraints, and a comparator for ordering the
* list.
*
* @param propositionIds
* a {@link String[]} of proposition ids.
* @param constraints
* a {@link PropertyConstraint[]} of constraints.
* @param comparator
* a {@link Comparator}.
* @param fromIndex
* the lower bound of the range (a negative number or zero is
* interpreted as the beginning of the list).
* @param toIndex
* the upper bound of the range exclusive (a negative number is
* interpreted as the end of the list).
*/
Link(String[] propositionIds, PropertyConstraint[] constraints,
Comparator comparator, int fromIndex, int toIndex) {
if (propositionIds == null) {
this.propIdsAsSet = Collections.emptySet();
} else {
ProtempaUtil.checkArrayForNullElement(propositionIds,
"propositionIds");
this.propIdsAsSet = new HashSet<>();
for (String propId : propositionIds) {
this.propIdsAsSet.add(propId.intern());
}
}
if (constraints == null) {
this.constraints = EMPTY_PROPERTY_CONSTRAINT_ARR;
} else {
ProtempaUtil.checkArrayForNullElement(constraints, "constraints");
this.constraints = constraints.clone();
}
if (fromIndex >= 0 && toIndex >= 0 && fromIndex >= toIndex) {
throw new IllegalArgumentException(
"fromIndex cannot be greater than or equal to toIndex");
}
this.comparator = comparator;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
/**
* The ids of the propositions to traverse to. An empty array indicates
* that all propositions are of interest.
*
* @return a proposition id {@link String[]} Guaranteed not
* null
.
*/
public final String[] getPropositionIds() {
return this.propIdsAsSet.toArray(new String[this.propIdsAsSet.size()]);
}
/**
* Aggregates and returns the possible proposition ids on the right-hand
* side of the link.
*
* @param inPropIds the proposition ids inferred from the previous link,
* or the row proposition id if this is the first link.
*
* @return an array of proposition id {@link String}s.
*/
public abstract String[] getInferredPropositionIds(
KnowledgeSource knowledgeSource, String[] inPropIds)
throws KnowledgeSourceReadException;
/**
* Returns whether a proposition has one of the proposition ids specified
* by the link.
*
* @param proposition a {@link Proposition}. Cannot be null
.
* @return true
or false
.
*/
protected boolean isMatch(Proposition proposition) {
return this.propIdsAsSet.isEmpty() ||
this.propIdsAsSet.contains(proposition.getId());
}
/**
* Constraints on properties of the propositions at the end of the
* traversal.
*
* @return a {@link PropertyConstraint[]}. Guaranteed not null
.
*/
public final PropertyConstraint[] getConstraints() {
return this.constraints.clone();
}
/**
* A comparator for ordering the propositions at the end of the traversal.
* If null
, the propositions will not be provided in any
* particular order.
*
* @return a {@link Comparator}.
*/
public final Comparator getComparator() {
return this.comparator;
}
/**
* The start index of the propositions of interest at the end of the
* traversal step, after applying the proposition id constraints, property
* constraints and a comparator.
*
* @return an int
, with zero or a negative number indicating
* the beginning of the list.
*/
public final int getFromIndex() {
return this.fromIndex;
}
/**
* The last index of the propositions of interest at the end of the
* traversal step exclusive, after applying the proposition id constraints,
* property constraints and a comparator.
*
* @return an int
, with a negative number indicating the end of
* the list. Must be greater than the fromIndex
.
*/
public final int getToIndex() {
return this.toIndex;
}
/**
* Validates the fields of this link specification against the
* knowledge source.
*
* @param knowledgeSource a {@link KnowledgeSource}. Guaranteed not
* null
.
*
* @throws QueryResultsHandlerValidationFailedException if validation
* failed.
* @throws KnowledgeSourceReadException if the knowledge source could
* not be read.
*/
void validate(KnowledgeSource knowledgeSource) throws
LinkValidationFailedException, KnowledgeSourceReadException {
List invalidPropIds = new ArrayList<>();
List propDefs = knowledgeSource.readPropositionDefinitions(this.propIdsAsSet.toArray(new String[this.propIdsAsSet.size()]));
Set foundPropIds = new HashSet<>();
for (PropositionDefinition propDef : propDefs) {
foundPropIds.add(propDef.getId());
}
for (String propId : this.propIdsAsSet) {
if (!foundPropIds.contains(propId)) {
invalidPropIds.add(propId);
}
}
if (!invalidPropIds.isEmpty()) {
throw new LinkValidationFailedException(
"Invalid proposition id(s): " +
StringUtils.join(invalidPropIds, ", "));
}
}
/**
* Generates a string for
* {@link org.protempa.query.handler.TableQueryResultsHandler} column
* headers.
*
* @return a {@link String}.
*/
abstract String headerFragment();
/**
* Returns the default header fragment for this link.
*
* @param ref
* the name {@link String} of the reference of this link.
* @return a header fragment {@link String}.
* @see #headerFragment()
*/
final String createHeaderFragment(String ref) {
int size = this.propIdsAsSet.size();
boolean sep1Needed = size > 0 && this.constraints.length > 0;
String sep1 = sep1Needed ? ", " : "";
String id = size > 0 ? "id=" : "";
boolean parenNeeded = size > 0 || this.constraints.length > 0;
String startParen = parenNeeded ? "(" : "";
String finishParen = parenNeeded ? ")" : "";
String range = rangeString();
boolean sep2Needed = sep1Needed && range.length() > 0;
String sep2 = sep2Needed ? ", " : "";
return '.' + ref + startParen + id
+ StringUtils.join(this.propIdsAsSet, ',') + sep1
+ constraintHeaderString(this.constraints) + finishParen + sep2
+ range;
}
/**
* Filters out propositions by applying the property constraint and index
* constraints. Modifies propositions
in-place!
*
* @param propositions
* a {@link Collection}. If null
, an
* empty list is returned.
*
* @return a {@link List}. Not guaranteed to be modifiable.
*/
protected final List createResults(
List propositions) {
List result;
if (propositions != null) {
if (this.constraints.length > 0) {
result = new ArrayList<>();
for (Proposition prop : propositions) {
applyPropertyConstraints(prop, result);
}
} else {
result = propositions;
}
result = applyComparatorAndIndices(result);
} else {
result = Collections.emptyList();
}
return result;
}
private String constraintHeaderString(PropertyConstraint[] constraints) {
List constraintsL = new ArrayList<>(constraints.length);
for (int i = 0; i < constraints.length; i++) {
PropertyConstraint ccc = constraints[i];
constraintsL.add(ccc.getFormatted());
}
return StringUtils.join(constraintsL, ',');
}
private String rangeString() {
boolean rangeSpecified = this.fromIndex >= 0 || this.toIndex >= 0;
String range = rangeSpecified ? "range=" : "";
if (rangeSpecified) {
if (this.fromIndex >= 0) {
range += this.fromIndex;
} else {
range += 0;
}
range += ",";
if (this.toIndex >= 0) {
range += this.toIndex;
} else {
range += "end";
}
}
return range;
}
private List applyComparatorAndIndices(
List result) {
assert result != null : "result should not be null in sliceResults";
if (!result.isEmpty()) {
if (this.comparator != null) {
Collections.sort(result, this.comparator);
}
if (this.fromIndex >= 0 || this.toIndex >= 0) {
int resultSize = result.size();
return result.subList(
this.fromIndex >= 0 ? this.fromIndex : 0,
this.toIndex >= 0 ? Math.min(this.toIndex, resultSize)
: resultSize);
}
}
return result;
}
private void applyPropertyConstraints(Proposition prop,
Collection result) {
assert prop != null : "prop cannot be null";
assert result != null : "result cannot be null";
boolean compatible = constraintsCheckCompatible(prop,this.constraints);
if (compatible) {
result.add(prop);
}
}
private boolean constraintsCheckCompatible(Proposition proposition,
PropertyConstraint[] constraints) {
Logger logger = Util.logger();
for (PropertyConstraint ccc : constraints) {
String propName = ccc.getPropertyName();
Value value = proposition.getProperty(propName);
if (value != null) {
ValueComparator vc = ccc.getValueComparator();
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER,
"Proposition is {0}; Property is {1}; Value is {2}; Comparator: {3}",
new Object[] { proposition.getId(), propName,
value, vc });
}
if (!vc.compare(value, ccc.getValue())) {
return false;
}
} else {
return false;
}
}
return true;
}
/**
* Traverses the specified link from a proposition to a collection of
* propositions.
*
* @param proposition
* a {@link Proposition} at which to start the traversal.
* @param forwardDerivations
* a {@link Map>} of derived
* propositions.
* @param backwardDerivations
* a {@link Map>} of derived
* propositions.
* @param references
* a {@link Map} of unique identifiers
* to {@link Proposition}s, used to resolve references.
* @param knowledgeSource
* the {@link KnowledgeSource}.
* @param cache
* a {@link Set} for convenience in checking if
* duplicate propositions are traversed to. It is cleared
* in between calls to this method.
* @return the {@link Collection} at the end of the traversal
* step. Not guaranteed to be modifiable.
*/
abstract Collection traverse(Proposition proposition,
Map> forwardDerivations,
Map> backwardDerivations,
Map references,
KnowledgeSourceCache ksCache, Set cache);
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((comparator == null) ? 0 : comparator.hashCode());
result = prime * result + Arrays.hashCode(constraints);
result = prime * result + fromIndex;
result = prime * result + ((propIdsAsSet == null) ? 0 : propIdsAsSet.hashCode());
result = prime * result + toIndex;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Link other = (Link) obj;
if (comparator == null) {
if (other.comparator != null)
return false;
} else if (!comparator.equals(other.comparator))
return false;
if (!Arrays.equals(constraints, other.constraints))
return false;
if (fromIndex != other.fromIndex)
return false;
if (propIdsAsSet == null) {
if (other.propIdsAsSet != null)
return false;
} else if (!propIdsAsSet.equals(other.propIdsAsSet))
return false;
if (toIndex != other.toIndex)
return false;
return true;
}
}