/*
* Copyright 2010 Outerthought bvba
*
* 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.
*/
package org.lilyproject.indexer.model.indexerconf;
import javax.xml.XMLConstants;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.lilyproject.repository.api.FieldType;
import org.lilyproject.repository.api.FieldTypeNotFoundException;
import org.lilyproject.repository.api.LRepository;
import org.lilyproject.repository.api.QName;
import org.lilyproject.repository.api.RepositoryException;
import org.lilyproject.repository.api.SchemaId;
import org.lilyproject.repository.api.Scope;
import org.lilyproject.repository.api.TypeManager;
import org.lilyproject.util.location.LocationAttributes;
import org.lilyproject.util.repo.FieldValueStringConverter;
import org.lilyproject.util.repo.SystemFields;
import org.lilyproject.util.repo.VersionTag;
import org.lilyproject.util.xml.DocumentHelper;
import org.lilyproject.util.xml.LocalXPathExpression;
import org.lilyproject.util.xml.XPathUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
// Terminology: the word "field" is usually used for a field from a repositoryManager record, while
// the term "index field" is usually used for a field in the index, though sometimes these
// are also just called field.
public class LilyIndexerConfBuilder {
private static LocalXPathExpression INDEX_CASES =
new LocalXPathExpression("/indexer/records/record");
private static LocalXPathExpression RECORD_INCLUDE_FILTERS =
new LocalXPathExpression("/indexer/recordFilter/includes/include");
private static LocalXPathExpression RECORD_EXCLUDE_FILTERS =
new LocalXPathExpression("/indexer/recordFilter/excludes/exclude");
private static LocalXPathExpression FORMATTERS =
new LocalXPathExpression("/indexer/formatters/formatter");
private static LocalXPathExpression INDEX_FIELDS =
new LocalXPathExpression("/indexer/fields");
private static LocalXPathExpression DYNAMIC_INDEX_FIELDS =
new LocalXPathExpression("/indexer/dynamicFields/dynamicField");
private static final Splitter COMMA_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
private static final Splitter EQUAL_SIGN_SPLITTER = Splitter.on('=').trimResults().omitEmptyStrings();
private static final Splitter DEREF_SIGN_SPLITTER = Splitter.on("=>").trimResults().omitEmptyStrings();
private final Log log = LogFactory.getLog(getClass());
private Document doc;
private LilyIndexerConf conf;
private LRepository repository;
private TypeManager typeManager;
private SystemFields systemFields;
private LilyIndexerConfBuilder() {
// prevents instantiation
}
public static LilyIndexerConf build(InputStream is, LRepository repository) throws IndexerConfException {
Document doc;
try {
doc = DocumentHelper.parse(is);
} catch (Exception e) {
throw new IndexerConfException("Error parsing supplied configuration.", e);
}
return new LilyIndexerConfBuilder().build(doc, repository);
}
private LilyIndexerConf build(Document doc, LRepository repository) throws IndexerConfException {
validate(doc);
this.doc = doc;
this.repository = repository;
this.typeManager = repository.getTypeManager();
this.systemFields = SystemFields.getInstance(repository.getTypeManager(), repository.getIdGenerator());
this.conf = new LilyIndexerConf();
this.conf.setSystemFields(systemFields);
try {
buildRecordFilter();
buildFormatters();
buildIndexFields();
buildDynamicFields();
} catch (Exception e) {
throw new IndexerConfException("Error in the configuration.", e);
}
return conf;
}
private void buildRecordFilter() throws Exception {
IndexRecordFilter recordFilter = new IndexRecordFilter();
List<Element> includes = RECORD_INCLUDE_FILTERS.get().evalAsNativeElementList(doc);
for (Element includeEl : includes) {
RecordMatcher recordMatcher = parseRecordMatcher(includeEl);
String vtagsSpec = DocumentHelper.getAttribute(includeEl, "vtags", true);
Set<SchemaId> vtags = parseVersionTags(vtagsSpec);
recordFilter.addInclude(recordMatcher, new IndexCase(vtags));
}
List<Element> excludes = RECORD_EXCLUDE_FILTERS.get().evalAsNativeElementList(doc);
for (Element excludeEl : excludes) {
RecordMatcher recordMatcher = parseRecordMatcher(excludeEl);
recordFilter.addExclude(recordMatcher);
}
// This is for backwards compatibility: previously, <recordFilter> was called <records> and didn't have
// excludes. This syntax was deprecated in 2.0.
List<Element> cases = INDEX_CASES.get().evalAsNativeElementList(doc);
for (Element caseEl : cases) {
WildcardPattern matchNamespace = null;
WildcardPattern matchName = null;
String matchNamespaceAttr = DocumentHelper.getAttribute(caseEl, "matchNamespace", false);
if (matchNamespaceAttr != null) {
// If the matchNamespace attr does not contain a wildcard expression, and its value
// happens to be an existing namespace prefix, than substitute the prefix for the full URI.
if (!WildcardPattern.isWildcardExpression(matchNamespaceAttr)) {
String uri = caseEl.lookupNamespaceURI(matchNamespaceAttr);
if (uri != null) {
matchNamespaceAttr = uri;
}
}
matchNamespace = new WildcardPattern(matchNamespaceAttr);
}
String matchNameAttr = DocumentHelper.getAttribute(caseEl, "matchName", false);
if (matchNameAttr != null) {
matchName = new WildcardPattern(matchNameAttr);
}
String vtagsSpec = DocumentHelper.getAttribute(caseEl, "vtags", false);
Map<String, String> varPropsPattern = parseVariantPropertiesPattern(caseEl, "matchVariant");
Set<SchemaId> vtags = parseVersionTags(vtagsSpec);
RecordMatcher recordMatcher = new RecordMatcher(matchNamespace, matchName, null, null, null, null,
varPropsPattern, null, typeManager);
recordFilter.addInclude(recordMatcher, new IndexCase(vtags));
}
conf.setRecordFilter(recordFilter);
}
private RecordMatcher parseRecordMatcher(Element element) throws Exception {
//
// Condition on record type
//
WildcardPattern rtNamespacePattern = null;
WildcardPattern rtNamePattern = null;
String recordTypeAttr = DocumentHelper.getAttribute(element, "recordType", false);
if (recordTypeAttr != null) {
QName rtName = ConfUtil.parseQName(recordTypeAttr, element, true);
rtNamespacePattern = new WildcardPattern(rtName.getNamespace());
rtNamePattern = new WildcardPattern(rtName.getName());
}
//
// "Instance of" condition
//
String instanceOfAttr = DocumentHelper.getAttribute(element, "instanceOf", false);
QName instanceOfType = null;
if (instanceOfAttr != null) {
instanceOfType = ConfUtil.parseQName(instanceOfAttr, element, false);
}
List<String> tableNames = extractTableNames(DocumentHelper.getAttribute(element, "tables", false));
//
// Condition on variant properties
//
Map<String, String> varPropsPattern = parseVariantPropertiesPattern(element, "variant");
//
// Condition on field
//
String fieldAttr = DocumentHelper.getAttribute(element, "field", false);
FieldType fieldType = null;
RecordMatcher.FieldComparator comparator = null;
Object fieldValue = null;
if (fieldAttr != null) {
int eqPos = fieldAttr.indexOf('='); // we assume = is not a symbol occurring in the field name
if (eqPos == -1) {
throw new IndexerConfException("field test should be of the form \"namespace:name(=|!=)value\", which " +
"the following is not: " + fieldAttr + ", at " + LocationAttributes.getLocation(element));
}
// not-equals support (simplistic parsing approach, doesn't need anything more complex for now)
String namePart = fieldAttr.substring(0, eqPos);
if (namePart.endsWith("!")) {
namePart = namePart.substring(0, namePart.length() - 1);
comparator = RecordMatcher.FieldComparator.NOT_EQUAL;
} else {
comparator = RecordMatcher.FieldComparator.EQUAL;
}
QName fieldName = ConfUtil.parseQName(namePart, element);
fieldType = typeManager.getFieldTypeByName(fieldName);
String fieldValueString = fieldAttr.substring(eqPos + 1);
try {
fieldValue = FieldValueStringConverter.fromString(fieldValueString, fieldType.getValueType(),
repository.getIdGenerator());
} catch (IllegalArgumentException e) {
throw new IndexerConfException("Invalid field value: " + fieldValueString);
}
}
return new RecordMatcher(rtNamespacePattern, rtNamePattern, instanceOfType, fieldType,
comparator, fieldValue, varPropsPattern, tableNames, typeManager);
}
private void buildFormatters() throws Exception {
List<Element> formatters = FORMATTERS.get().evalAsNativeElementList(doc);
for (Element formatterEl : formatters) {
String className = DocumentHelper.getAttribute(formatterEl, "class", true);
Formatter formatter = instantiateFormatter(className);
String name = DocumentHelper.getAttribute(formatterEl, "name", true);
if (name != null && conf.getFormatters().hasFormatter(name)) {
throw new IndexerConfException("Duplicate formatter name: " + name);
}
conf.getFormatters().addFormatter(formatter, name);
}
String defaultFormatter = XPathUtils.evalString("/indexer/formatters/@default", doc);
if (defaultFormatter.length() != 0) {
conf.getFormatters().setDefaultFormatter(defaultFormatter);
}
}
private Formatter instantiateFormatter(String className) throws IndexerConfException {
ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
Class formatterClass;
try {
formatterClass = contextCL.loadClass(className);
} catch (ClassNotFoundException e) {
throw new IndexerConfException("Error loading formatter class " + className + " from context class loader.",
e);
}
if (!Formatter.class.isAssignableFrom(formatterClass)) {
throw new IndexerConfException(
"Specified formatter class does not implement Formatter interface: " + className);
}
try {
return (Formatter) formatterClass.newInstance();
} catch (Exception e) {
throw new IndexerConfException("Error instantiating formatter class " + className, e);
}
}
private Map<String, String> parseVariantPropertiesPattern(Element caseEl, String attrName) throws Exception {
String variant = DocumentHelper.getAttribute(caseEl, attrName, false);
if (variant == null) {
return null;
}
Map<String, String> varPropsPattern = new HashMap<String, String>();
for (String prop : COMMA_SPLITTER.split(variant)) {
int eqPos = prop.indexOf("=");
if (eqPos != -1) {
String propName = prop.substring(0, eqPos);
String propValue = prop.substring(eqPos + 1);
if (propName.equals("*")) {
throw new IndexerConfException(String.format("Error in " + attrName +
" attribute: the character '*' can only be used as wildcard, not as variant dimension " +
"name, attribute = %1$s, at: %2$s", variant, LocationAttributes.getLocation(caseEl)));
}
varPropsPattern.put(propName, propValue);
} else {
varPropsPattern.put(prop, null);
}
}
return varPropsPattern;
}
private Set<SchemaId> parseVersionTags(String vtagsSpec) throws IndexerConfException, InterruptedException {
Set<SchemaId> vtags = new HashSet<SchemaId>();
if (vtagsSpec == null) {
return vtags;
}
for (String tag : COMMA_SPLITTER.split(vtagsSpec)) {
try {
vtags.add(typeManager.getFieldTypeByName(VersionTag.qname(tag)).getId());
} catch (FieldTypeNotFoundException e) {
throw new IndexerConfException("unknown vtag used in indexer configuration: " + tag);
} catch (RepositoryException e) {
throw new IndexerConfException("error loading field type for vtag: " + tag, e);
}
}
return Collections.unmodifiableSet(vtags);
}
private void buildIndexFields() throws Exception {
conf.setIndexFields(buildIndexFields(INDEX_FIELDS.get().evalAsNativeElement(doc)));
}
public IndexFields buildIndexFields(Element el) throws Exception {
IndexFields indexFields = new IndexFields();
if (el != null) {
addChildNodes(el, indexFields, "match", "field", "forEach");
}
return indexFields;
}
private MatchNode buildMatchNode(Element el) throws Exception {
RecordMatcher recordMatcher = parseRecordMatcher(el);
MatchNode matchNode = new MatchNode(recordMatcher);
addChildNodes(el, matchNode, "match", "field", "forEach");
return matchNode;
}
/**
* @param forwardVariantDimensions in case this is an index field which is part of a foreach, these are the forward
* variant dimensions used in the foreach, and thus the ones that can be used to
* build a name from a template. Otherwise <code>null</code>.
*/
private IndexField buildIndexField(Element el, Set<String> forwardVariantDimensions) throws Exception {
String nameAttr = DocumentHelper.getAttribute(el, "name", true);
String valueExpr = DocumentHelper.getAttribute(el, "value", true);
final Set<QName> supportedFields = new HashSet<QName>();
supportedFields.addAll(systemFields.getAll());
supportedFields.addAll(getAllRepositoryFields());
NameTemplate name = new NameTemplateParser(repository, systemFields)
.parse(el, nameAttr, new FieldNameTemplateValidator(forwardVariantDimensions, supportedFields));
return new IndexField(name, buildValue(el, valueExpr));
}
private Set<QName> getAllRepositoryFields() throws RepositoryException, InterruptedException {
final Set<QName> result = new HashSet<QName>();
for (FieldType fieldType : repository.getTypeManager().getFieldTypes()) {
result.add(fieldType.getName());
}
return result;
}
private ForEachNode buildForEachNode(Element el) throws Exception {
String expr = DocumentHelper.getAttribute(el, "expr", true);
Follow follow = null;
try {
follow = parseFollow(el, expr);
} catch (Exception e) {
throw new IndexerConfException("Failed to process forEach element at " + LocationAttributes.getLocationString(el), e);
}
ForEachNode forEachNode = new ForEachNode(systemFields, follow);
addChildNodes(el, forEachNode, "field", "match", "forEach");
return forEachNode;
}
public void addChildNodes(Element el, ContainerMappingNode parent, String... allowedTagNames) throws Exception {
Set<String> allowed = Sets.newHashSet(allowedTagNames);
for (Element childEl: DocumentHelper.getElementChildren(el)) {
String name = childEl.getTagName();
if (!allowed.contains(name)) {
throw new IndexerConfException(String.format("Unexpected tag name '%s' while parsing indexerconf", childEl.getTagName()));
}
if (name.equals("fields")) {
parent.addChildNode(buildIndexFields(childEl));
} else if (name.equals("match")) {
parent.addChildNode(buildMatchNode(childEl));
} else if (name.equals("field")) {
final IndexField indexField;
if (parent instanceof ForEachNode && ((ForEachNode) parent).getFollow() instanceof ForwardVariantFollow) {
indexField = buildIndexField(childEl, ((ForwardVariantFollow) ((ForEachNode)parent).getFollow()).getDimensions().keySet());
} else {
indexField = buildIndexField(childEl, null);
}
parent.addChildNode(indexField);
} else if (name.equals("forEach")) {
parent.addChildNode(buildForEachNode(childEl));
} else {
throw new IndexerConfException(String.format("Unexpected tag name '%s' while parsing indexerconf", childEl.getTagName()));
}
}
}
private void buildDynamicFields() throws Exception {
List<Element> fields = DYNAMIC_INDEX_FIELDS.get().evalAsNativeElementList(doc);
for (Element fieldEl : fields) {
String matchNamespaceAttr = DocumentHelper.getAttribute(fieldEl, "matchNamespace", false);
String matchNameAttr = DocumentHelper.getAttribute(fieldEl, "matchName", false);
String matchTypeAttr = DocumentHelper.getAttribute(fieldEl, "matchType", false);
String matchScopeAttr = DocumentHelper.getAttribute(fieldEl, "matchScope", false);
String nameAttr = DocumentHelper.getAttribute(fieldEl, "name", true);
WildcardPattern matchNamespace = null;
if (matchNamespaceAttr != null) {
// If the matchNamespace attr does not contain a wildcard expression, and its value
// happens to be an existing namespace prefix, than substitute the prefix for the full URI.
if (!WildcardPattern.isWildcardExpression(matchNamespaceAttr)) {
String uri = fieldEl.lookupNamespaceURI(matchNamespaceAttr);
if (uri != null) {
matchNamespaceAttr = uri;
}
}
matchNamespace = new WildcardPattern(matchNamespaceAttr);
}
WildcardPattern matchName = null;
if (matchNameAttr != null) {
matchName = new WildcardPattern(matchNameAttr);
}
TypePattern matchTypes = null;
if (matchTypeAttr != null) {
matchTypes = new TypePattern(matchTypeAttr);
}
Set<Scope> matchScopes = null;
if (matchScopeAttr != null) {
matchScopes = EnumSet.noneOf(Scope.class);
for (String scope : COMMA_SPLITTER.split(matchScopeAttr)) {
matchScopes.add(Scope.valueOf(scope));
}
if (matchScopes.isEmpty()) {
matchScopes = null;
}
}
// Be gentle to users of Lily 1.0 and warn them about attributes that are not supported anymore
if (DocumentHelper.getAttribute(fieldEl, "matchMultiValue", false) != null) {
log.warn("The attribute matchMultiValue on dynamicField is not supported anymore, it will be ignored.");
}
if (DocumentHelper.getAttribute(fieldEl, "matchHierarchical", false) != null) {
log.warn(
"The attribute matchHierarchical on dynamicField is not supported anymore, it will be ignored.");
}
Set<String> variables = new HashSet<String>();
variables.add("namespace");
variables.add("name");
variables.add("type");
variables.add("baseType");
variables.add("nestedType");
variables.add("nestedBaseType");
variables.add("deepestNestedBaseType");
if (matchName != null && matchName.hasWildcard()) {
variables.add("nameMatch");
}
if (matchNamespace != null && matchNamespace.hasWildcard()) {
variables.add("namespaceMatch");
}
NameTemplate name;
try {
name = new NameTemplateParser().parse(fieldEl, nameAttr, new DynamicFieldNameTemplateValidator(variables));
} catch (NameTemplateException nte) {
throw new IndexerConfException("Error in name template: " + nameAttr + " at " + LocationAttributes.getLocationString(fieldEl), nte);
}
boolean extractContent = DocumentHelper.getBooleanAttribute(fieldEl, "extractContent", false);
String formatter = DocumentHelper.getAttribute(fieldEl, "formatter", false);
if (formatter != null && !conf.getFormatters().hasFormatter(formatter)) {
throw new IndexerConfException("Formatter does not exist: " + formatter + " at " +
LocationAttributes.getLocationString(fieldEl));
}
boolean continue_ = DocumentHelper.getBooleanAttribute(fieldEl, "continue", false);
DynamicIndexField field = new DynamicIndexField(matchNamespace, matchName, matchTypes,
matchScopes, name, extractContent, continue_, formatter);
conf.addDynamicIndexField(field);
}
}
private void validateName(String name) throws IndexerConfException {
//FIXME: seems like a useful validation, but not called any more?
if (name.startsWith("lily.")) {
throw new IndexerConfException("names starting with 'lily.' are reserved for internal uses. Name: " + name);
}
}
private Value buildValue(Element fieldEl, String valueExpr) throws Exception {
Value value;
boolean extractContent = DocumentHelper.getBooleanAttribute(fieldEl, "extractContent", false);
String formatter = DocumentHelper.getAttribute(fieldEl, "formatter", false);
if (formatter != null && !conf.getFormatters().hasFormatter(formatter)) {
throw new IndexerConfException("Formatter does not exist: " + formatter + " at " +
LocationAttributes.getLocationString(fieldEl));
}
//
// An index field can basically map to two kinds of values:
// * plain field values
// * dereference expressions (following links to some other record and then taking a field value from it)
//
// A dereference expression is specified as "somelink=>somelink=>somefield"
if (valueExpr.contains("=>")) {
//
// A dereference field
//
value = buildDerefValue(fieldEl, valueExpr, extractContent, formatter);
} else {
//
// A plain field
//
value = new FieldValue(ConfUtil.getFieldType(valueExpr, fieldEl, systemFields, typeManager), extractContent, formatter);
}
if (extractContent &&
!value.getTargetFieldType().getValueType().getDeepestValueType().getBaseName().equals("BLOB")) {
throw new IndexerConfException("extractContent is used for a non-blob value at "
+ LocationAttributes.getLocation(fieldEl));
}
return value;
}
private Value buildDerefValue(Element fieldEl, String valueExpr, boolean extractContent, String formatter)
throws Exception {
final String[] derefParts = parseDerefParts(fieldEl, valueExpr);
final List<Follow> follows = parseFollows(fieldEl, valueExpr, derefParts);
final Value value = buildValue(fieldEl, derefParts[derefParts.length - 1]);
boolean lastFollowIsRecord = false;
for (Follow follow: follows) {
if (lastFollowIsRecord) {
if (follow instanceof VariantFollow ||
follow instanceof ForwardVariantFollow ||
follow instanceof MasterFollow) {
String locationString = LocationAttributes.getLocationString(fieldEl);
throw new IndexerConfException("In deref expressions, a variant(+/-/master) follow" +
" cannot follow after a record field. Location: " + locationString);
}
}
if (follow instanceof RecordFieldFollow) {
lastFollowIsRecord = true;
} else {
lastFollowIsRecord = false;
}
}
// If the last follow is a RecordFieldFollow, we check that the Value isn't something which requires a real Record
if (lastFollowIsRecord) {
SchemaId fieldDependency = value.getFieldDependency();
if (systemFields.isSystemField(fieldDependency)) {
checkSystemFieldUsage(fieldEl, valueExpr, fieldDependency, new QName(SystemFields.NS, "id"));
checkSystemFieldUsage(fieldEl, valueExpr, fieldDependency, new QName(SystemFields.NS, "link"));
}
}
final FieldType fieldType = constructDerefFieldType(fieldEl, valueExpr, derefParts);
final DerefValue deref = new DerefValue(follows, value, fieldType, extractContent, formatter);
deref.init(typeManager);
return deref;
}
private void checkSystemFieldUsage(Element fieldEl, String valueExpr, SchemaId fieldDependency, QName field)
throws FieldTypeNotFoundException, IndexerConfException {
if (fieldDependency.equals(systemFields.get(field))) {
throw new IndexerConfException("In dereferencing, " + field + " cannot follow on record-type field." +
" Deref expression: '" + valueExpr + "' at " + LocationAttributes.getLocation(fieldEl));
}
}
private List<Follow> parseFollows(Element fieldEl, String valueExpr, String[] derefParts) throws Exception {
List<Follow> follows = new ArrayList<Follow>();
try {
for (int i = 0; i < derefParts.length - 1; i++) {
String derefPart = derefParts[i];
// A deref expression can navigate through 5 kinds of 'links':
// - a link stored in a link field (detected based on presence of a colon)
// - a nested record
// - a link to the master variant (if it's the literal string 'master')
// - a link to a less-dimensioned variant
// - a link to a more-dimensioned variant
follows.add(parseFollow(fieldEl, derefPart));
}
} catch (Exception e) {
throw new IndexerConfException("Failed to parse deref expression at " + LocationAttributes.getLocationString(fieldEl));
}
return follows;
}
private Follow parseFollow(Element fieldEl, String derefPart) throws IndexerConfException, InterruptedException, RepositoryException {
if (derefPart.contains(":") || derefPart.startsWith("{")) { // It's a field name
return processFieldDeref(fieldEl, derefPart);
} else if (derefPart.equals("master")) { // Link to master variant
return new MasterFollow();
} else if (derefPart.trim().startsWith("-")) { // Link to less dimensioned variant
return processLessDimensionedVariantsDeref(derefPart);
} else if (derefPart.trim().startsWith("+")) { // Link to more dimensioned variant
return processMoreDimensionedVariantsDeref(derefPart);
} else {
throw new IndexerConfException("I don't know how handle the part '" + derefPart + "'");
}
}
private String[] parseDerefParts(Element fieldEl, String valueExpr) throws IndexerConfException {
//
// Split, normalize, validate the input
//
String[] derefParts = Iterables.toArray(DEREF_SIGN_SPLITTER.split(valueExpr), String.class);
for (int i = 0; i < derefParts.length; i++) {
String trimmed = derefParts[i].trim();
if (trimmed.length() == 0) {
throw new IndexerConfException("Invalid dereference expression '" + valueExpr + "' at "
+ LocationAttributes.getLocationString(fieldEl));
}
derefParts[i] = trimmed;
}
if (derefParts.length < 2) {
throw new IndexerConfException("Invalid dereference expression '" + valueExpr + "' at "
+ LocationAttributes.getLocationString(fieldEl));
}
return derefParts;
}
private FieldType constructDerefFieldType(Element fieldEl, String valueExpr, String[] derefParts)
throws IndexerConfException, InterruptedException, RepositoryException {
//
// Last element in the list should be a field
//
QName targetFieldName;
try {
targetFieldName = ConfUtil.parseQName(derefParts[derefParts.length - 1], fieldEl);
} catch (IndexerConfException e) {
throw new IndexerConfException("Dereference expression does not end on a valid field name. " +
"Expression: '" + valueExpr + "' at " + LocationAttributes.getLocationString(fieldEl), e);
}
return ConfUtil.getFieldType(targetFieldName, systemFields, typeManager);
}
private Follow processFieldDeref(Element fieldEl, String derefPart)
throws IndexerConfException, InterruptedException, RepositoryException {
FieldType followField = ConfUtil.getFieldType(derefPart, fieldEl, systemFields, typeManager);
String type = followField.getValueType().getBaseName();
if (type.equals("LIST")) {
type = followField.getValueType().getNestedValueType().getBaseName();
}
if (type.equals("RECORD")) {
return new RecordFieldFollow(followField);
} else if (type.equals("LINK")) {
return new LinkFieldFollow(followField);
} else {
throw new IndexerConfException("Dereferencing is not possible on field of type " +
followField.getValueType().getName() + ". Field: '" + derefPart);
}
}
private Follow processLessDimensionedVariantsDeref(String derefPart) throws IndexerConfException {
// The variant dimensions are specified in a syntax like "-var1,-var2,-var3"
boolean validConfig = true;
Set<String> dimensions = new HashSet<String>();
for (String op : COMMA_SPLITTER.split(derefPart)) {
if (op.length() > 1 && op.startsWith("-")) {
String dimension = op.substring(1);
dimensions.add(dimension);
} else {
validConfig = false;
break;
}
}
if (dimensions.size() == 0) {
validConfig = false;
}
if (!validConfig) {
throw new IndexerConfException("Invalid specification of variants to follow: '" + derefPart);
}
return new VariantFollow(dimensions);
}
private Follow processMoreDimensionedVariantsDeref(String derefPart) throws IndexerConfException {
// The variant dimension is specified in a syntax like "+var1=boo,+var2"
boolean validConfig = true;
Map<String, String> dimensions = new HashMap<String, String>();
for (String op : COMMA_SPLITTER.split(derefPart)) {
if (op.length() > 1 && op.startsWith("+")) {
final Iterator<String> keyAndValue = EQUAL_SIGN_SPLITTER.split(op).iterator();
if (keyAndValue.hasNext()) {
final String key = keyAndValue.next().substring(1); // ignore leading '+'
if (keyAndValue.hasNext()) {
// there is an equal sign -> key and value
final String value = keyAndValue.next();
dimensions.put(key, value);
} else {
// no equal sign -> only key without value
dimensions.put(key, null);
}
} else {
// nothing at all?
validConfig = false;
break;
}
} else {
validConfig = false;
break;
}
}
if (dimensions.size() == 0) {
validConfig = false;
}
if (!validConfig) {
throw new IndexerConfException("Invalid specification of variants to follow: '" + derefPart);
}
return new ForwardVariantFollow(dimensions);
}
private void validate(Document document) throws IndexerConfException {
try {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL url = getClass().getClassLoader()
.getResource("org/lilyproject/indexer/model/indexerconf/indexerconf.xsd");
Schema schema = factory.newSchema(url);
Validator validator = schema.newValidator();
validator.validate(new DOMSource(document));
} catch (Exception e) {
throw new IndexerConfException("Error validating indexer configuration against XML Schema.", e);
}
}
static List<String> extractTableNames(String tableNameAttr) {
if (tableNameAttr == null) {
return null;
}
List<String> tableNames = Lists.newArrayList();
tableNameAttr = tableNameAttr.trim();
for (String tableName : tableNameAttr.split(",")) {
tableName = tableName.trim();
if (!tableName.isEmpty()) {
tableNames.add(tableName);
}
}
return tableNames.isEmpty() ? null : tableNames;
}
public static void validate(InputStream is) throws IndexerConfException {
MyErrorHandler errorHandler = new MyErrorHandler();
try {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL url = LilyIndexerConfBuilder.class.getClassLoader()
.getResource("org/lilyproject/indexer/model/indexerconf/indexerconf.xsd");
Schema schema = factory.newSchema(url);
Validator validator = schema.newValidator();
validator.setErrorHandler(errorHandler);
validator.validate(new StreamSource(is));
} catch (Exception e) {
if (!errorHandler.hasErrors()) {
throw new IndexerConfException("Error validating indexer configuration.", e);
} // else it will be reported below
}
if (errorHandler.hasErrors()) {
throw new IndexerConfException("The following errors occurred validating the indexer configuration:\n" +
errorHandler.getMessage());
}
}
private static class MyErrorHandler implements ErrorHandler {
private final StringBuilder builder = new StringBuilder();
@Override
public void warning(SAXParseException exception) throws SAXException {
}
@Override
public void error(SAXParseException exception) throws SAXException {
addException(exception);
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
addException(exception);
}
public boolean hasErrors() {
return builder.length() > 0;
}
public String getMessage() {
return builder.toString();
}
private void addException(SAXParseException exception) {
if (builder.length() > 0) {
builder.append("\n");
}
builder.append("[").append(exception.getLineNumber()).append(":").append(exception.getColumnNumber());
builder.append("] ").append(exception.getMessage());
}
}
}