/* * 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()); } } }