com.redhat.lightblue.query.Projection Maven / Gradle / Ivy
/*
Copyright 2013 Red Hat, Inc. and/or its affiliates.
This file is part of lightblue.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package com.redhat.lightblue.query;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Joiner;
import com.redhat.lightblue.util.JsonObject;
import com.redhat.lightblue.util.MutablePath;
import com.redhat.lightblue.util.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
/**
* Base class for all projection objects
*/
public abstract class Projection extends JsonObject {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LoggerFactory.getLogger(Projection.class);
/**
* Inclusion status for a field
*
*
* - explicit_exclusion: the projection excludes this field
* explicitly
* - implicit_exzclusion: the projection excludes this field because an
* ancestor of the field is excluded
* - explicit_inclusion: the projection includes the field explicitly
* - explicit_exclusion: the projection excludes the field explicitly
* - undecided: the projection does not decide if the field should be
* included/excluded, and does not reference it
*
*/
public static enum Inclusion {
explicit_exclusion, implicit_exclusion, explicit_inclusion, implicit_inclusion, undecided
};
public static Projection fromJson(JsonNode node) {
if (node instanceof ArrayNode) {
return ProjectionList.fromJson((ArrayNode) node);
} else {
return BasicProjection.fromJson((ObjectNode) node);
}
}
/**
* Adds two projections and returns a new projection containing both. Any
* projection can be null. If the resulting projection is empty, returns
* null.
*/
public static Projection add(Projection p1, Projection p2) {
List list = new ArrayList<>();
if (p1 instanceof ProjectionList) {
list.addAll(((ProjectionList) p1).getItems());
} else if (p1 != null) {
list.add(p1);
}
if (p2 instanceof ProjectionList) {
list.addAll(((ProjectionList) p2).getItems());
} else if (p2 != null) {
list.add(p2);
}
return list.isEmpty() ? null : new ProjectionList(list);
}
/**
* Returns whether to include/exclude the field based on whether the field
* matches the pattern
*
* @param field The field name
* @param pattern The projection pattern
* @param inclusion flag to return if the field matches the given pattern
* @return value of inclusion
if field matches the pattern,
* else null
*
*
* field pattern result
* a * true
* a a true
* a b false
* a.b * false
* a.b *.b true
* a.b a.* true
* a.b *.* true
* a.b a.b true
*
*
* The return value is inclusion
if result is true,
* null
if result is false, meaning that whether the field
* should be included or not cannot be decided.
*/
public static Boolean fieldMatchesPattern(Path field, Path pattern, boolean inclusion) {
if (field.matches(pattern)) {
return inclusion;
} else {
return null;
}
}
/**
* If the field is an ancestor of the pattern, and if inclusion
* is true, returns true. Otherwise, returns null, meaning that whether the
* field is included or not cannot be decided.
*
* @param field The field name
* @param pattern The projection pattern
* @param inclusion flag to return if the field matches the given pattern
* @return TRUE if field is ancestor of the pattern and
* inclusion
is true, else null
*/
public static Boolean fieldAncestorOfPattern(Path field, Path pattern, boolean inclusion) {
if (field.matchingPrefix(pattern)) {
if (inclusion) {
return Boolean.TRUE;
} else {
return null;
}
} else {
return null;
}
}
/**
* Returns if the field should be included based on the recursive pattern.
*
* @param field The field name
* @param pattern The projection pattern
* @param inclusion flag to return if the field matches the given pattern
* @return Boolean value of inclusion
if field is in subtree of
* pattern
else null
*/
public static Boolean impliedInclusion(Path field, Path pattern, boolean inclusion) {
if (field.numSegments() > pattern.numSegments()
&& // If we're checking a field deeper than the pattern
// And if we're checking a field under the subtree of the pattern
field.prefix(pattern.numSegments()).matches(pattern)) {
return inclusion ? Boolean.TRUE : Boolean.FALSE;
} else {
return null;
}
}
/**
* Returns if the field should be included based on the pattern given.
*
* @param field The field whose inclusion is to be decided
* @param pattern The field pattern of projection
* @param inclusion If the projection expression includes the field. If
* false, the projection is for exclusion
* @param recursive If the projection is recursive
*
* @return implicit or explicit inclusion/exclusion, or undecided if
* projection does not decide if the field should be included/excluded
*/
public static Inclusion isFieldIncluded(Path field,
Path pattern,
boolean inclusion,
boolean recursive) {
// field match first, most specific type of check
Boolean v = fieldMatchesPattern(field, pattern, inclusion);
if (v != null) {
// if the last segment is ANY then inclusion/exclusion is implicit. else, inclusion/exclusion is explicit
if (pattern.tail(0).equals(Path.ANY)) {
return v ? Inclusion.implicit_inclusion : Inclusion.implicit_exclusion;
} else {
return v ? Inclusion.explicit_inclusion : Inclusion.explicit_exclusion;
}
}
// check field ancestor
v = fieldAncestorOfPattern(field, pattern, inclusion);
if (v != null) {
// is always explicit
return v ? Inclusion.explicit_inclusion : Inclusion.explicit_exclusion;
}
// if recursive, check for implied inclusion/exclusion
if (recursive) {
v = impliedInclusion(field, pattern, inclusion);
if (v != null) {
// recursive is always implicit
return v ? Inclusion.implicit_inclusion : Inclusion.implicit_exclusion;
}
}
return Inclusion.undecided;
}
/**
* Determines if the field is included in this projection
*
* @param field The absolute name of the field
*
* If the field name contains array indexes, they are converted to '*'
* before evaluation.
*/
public Inclusion getFieldInclusion(Path field) {
LOGGER.debug("Checking if {} is projected", field);
return getFieldInclusion(field, Path.EMPTY);
}
/**
* Determine if the field is explicitly included/excluded, implicitly
* included, or the projection does not decide on the field.
*/
public Inclusion getFieldInclusion(Path field, Path ctx) {
Path mfield = toMask(field);
ctx = toMask(ctx);
if (this instanceof FieldProjection) {
return getFieldInclusion(mfield, (FieldProjection) this, ctx);
} else if (this instanceof ArrayProjection) {
return getFieldInclusion(mfield, (ArrayProjection) this, ctx);
} else if (this instanceof ProjectionList) {
return getFieldInclusion(mfield, (ProjectionList) this, ctx);
}
return Inclusion.undecided;
}
/**
* Returns true if the field is needed to evaluate the projection
*/
public boolean isFieldRequiredToEvaluateProjection(Path field) {
LOGGER.debug("Checking if {} is referenced in projection", field);
return isFieldRequiredToEvaluateProjection(field, Path.EMPTY);
}
/**
* Returns true if the field is needed to evaluate the projection
*/
public boolean isFieldRequiredToEvaluateProjection(Path field, Path ctx) {
Path mfield = toMask(field);
ctx = toMask(ctx);
if (this instanceof FieldProjection) {
switch (getFieldInclusion(mfield, (FieldProjection) this, ctx)) {
case implicit_inclusion:
case explicit_inclusion:
return true;
default:
return false;
}
} else if (this instanceof ArrayQueryMatchProjection) {
if (getFieldInclusion(mfield, (ArrayProjection) this, ctx) == Inclusion.undecided) {
LOGGER.debug("whether to include {} is Undecided, checking projection query", mfield);
Path absField = new Path(ctx, toMask(((ArrayQueryMatchProjection) this).getField()));
Path nestedCtx = new Path(absField, Path.ANYPATH);
boolean ret = ((ArrayQueryMatchProjection) this).getMatch().isRequired(field, nestedCtx);
LOGGER.debug("isRequired({},{}.*={}", field, absField, ret);
if (ret) {
return true;
}
LOGGER.debug("Query does not require {}, checking nested projection", mfield);
if (((ArrayProjection) this).getProject() != null) {
ret = ((ArrayProjection) this).getProject().isFieldRequiredToEvaluateProjection(field, nestedCtx);
}
LOGGER.debug("result:{}", ret);
return ret;
} else {
return true;
}
} else if (this instanceof ArrayRangeProjection) {
return getFieldInclusion(mfield, (ArrayProjection) this, ctx) != Inclusion.undecided;
} else if (this instanceof ProjectionList) {
for (Projection x : ((ProjectionList) this).getItems()) {
if (x.isFieldRequiredToEvaluateProjection(field, ctx)) {
return true;
}
}
}
return false;
}
private Inclusion getFieldInclusion(Path field, ArrayProjection p, Path context) {
Path absField = new Path(context, toMask(p.getField()));
LOGGER.debug("Checking if array projection on {} projects {}", absField, field);
Inclusion inc = isFieldIncluded(field, absField, p.isInclude(), false);
Inclusion inc2 = p.getProject().getFieldInclusion(field, new Path(absField, Path.ANYPATH));
Inclusion ret;
if (inc == Inclusion.explicit_inclusion || inc2 == Inclusion.explicit_inclusion) {
ret = Inclusion.explicit_inclusion;
} else if (inc == Inclusion.implicit_inclusion || inc2 == Inclusion.implicit_inclusion) {
ret = Inclusion.implicit_inclusion;
} else if (inc == Inclusion.explicit_exclusion || inc2 == Inclusion.explicit_exclusion) {
ret = Inclusion.explicit_exclusion;
} else if (inc == Inclusion.implicit_exclusion || inc2 == Inclusion.implicit_exclusion) {
ret = Inclusion.implicit_exclusion;
} else {
ret = Inclusion.undecided;
}
LOGGER.debug("array projection on {} projects {}: {}", absField, field, ret);
return ret;
}
private Inclusion getFieldInclusion(Path field, ProjectionList p, Path context) {
LOGGER.debug("Checking if a projection list projects {}", field);
Inclusion lastResult = Inclusion.undecided;
List items = p.getItems();
ListIterator itemsItr = items.listIterator(items.size());
while (itemsItr.hasPrevious()) {
Inclusion ret = itemsItr.previous().getFieldInclusion(field, context);
if (ret != Inclusion.undecided) {
lastResult = ret;
break;
}
}
LOGGER.debug("Projection list projects {}: {}", field, lastResult);
return lastResult;
}
private Inclusion getFieldInclusion(Path field, FieldProjection p, Path context) {
Path projectionField = new Path(context, toMask(p.getField()));
LOGGER.debug("Checking if field projection on {} projects {}", projectionField, field);
Inclusion inc = isFieldIncluded(field, projectionField, p.isInclude(), p.isRecursive());
LOGGER.debug("Field projection on {} projects {}: {}", projectionField, field, inc);
return inc;
}
/**
* If a path includes array indexes, change the indexes into ANY
*/
private static Path toMask(Path p) {
int n = p.numSegments();
MutablePath mp = null;
for (int i = 0; i < n; i++) {
if (p.isIndex(i)) {
if (mp == null) {
mp = p.mutableCopy();
}
mp.set(i, Path.ANY);
}
}
return mp == null ? p : mp.immutableCopy();
}
protected static Path getNonRelativePath(Path p) {
List segments = new ArrayList<>();
int numberOfParentsOnPath = 0;
for (int i = p.numSegments() - 1; i >= 0; i--) {
if (Path.THIS.equals(p.head(i))) {
continue;
} else if (Path.PARENT.equals(p.head(i))) {
numberOfParentsOnPath++;
} else {
if (numberOfParentsOnPath > 0) {
numberOfParentsOnPath--;
continue;
}
segments.add(p.head(i));
}
}
Collections.reverse(segments);
return new Path(Joiner.on(".").join(segments));
}
}