/* * Licensed to STRATIO (C) under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. The STRATIO (C) 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 com.stratio.cassandra.lucene.schema.mapping; import com.google.common.base.MoreObjects; import com.stratio.cassandra.lucene.IndexException; import com.stratio.cassandra.lucene.column.Column; import com.stratio.cassandra.lucene.column.Columns; import com.stratio.cassandra.lucene.schema.analysis.StandardAnalyzers; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.db.marshal.CollectionType.Kind; import org.apache.commons.lang3.StringUtils; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field.Store; import org.apache.lucene.search.SortField; import java.nio.ByteBuffer; import java.util.List; import static org.apache.cassandra.db.marshal.CollectionType.Kind.LIST; import static org.apache.cassandra.db.marshal.CollectionType.Kind.SET; /** * Class for mapping between Cassandra's columns and Lucene documents. * * @author Andres de la Pena {@literal <adelapena@stratio.com>} */ public abstract class Mapper { /** A no-action getAnalyzer for not tokenized {@link Mapper} implementations. */ static final String KEYWORD_ANALYZER = StandardAnalyzers.KEYWORD.toString(); /** The store field in Lucene default option. */ public static final Store STORE = Store.NO; /** If the field must be indexed when no specified. */ public static final boolean DEFAULT_INDEXED = true; /** If the field must be sorted when no specified. */ public static final boolean DEFAULT_SORTED = false; /** If the field must be validated when no specified. */ public static final boolean DEFAULT_VALIDATED = false; /** The name of the Lucene field. */ public final String field; /** If the field must be indexed. */ public final Boolean indexed; /** If the field must be sorted. */ public final Boolean sorted; /** If the field must be validated. */ public final Boolean validated; /** The name of the analyzer to be used. */ public final String analyzer; /** The supported Cassandra types for indexing. */ public final AbstractType<?>[] supportedTypes; /** The names of the columns to be mapped. */ public final List<String> mappedColumns; /** * Builds a new {@link Mapper} supporting the specified types for indexing. * * @param field the name of the field * @param indexed if the field supports searching * @param sorted if the field supports sorting * @param validated if the field must be validated * @param analyzer the name of the analyzer to be used * @param mappedColumns the names of the columns to be mapped * @param supportedTypes the supported Cassandra types for indexing */ protected Mapper(String field, Boolean indexed, Boolean sorted, Boolean validated, String analyzer, List<String> mappedColumns, AbstractType<?>... supportedTypes) { if (StringUtils.isBlank(field)) { throw new IndexException("Field name is required"); } this.field = field; this.indexed = indexed == null ? DEFAULT_INDEXED : indexed; this.sorted = sorted == null ? DEFAULT_SORTED : sorted; this.validated = validated == null ? DEFAULT_VALIDATED : validated; this.analyzer = analyzer; this.mappedColumns = mappedColumns; this.supportedTypes = supportedTypes; } /** * Adds to the specified {@link Document} the Lucene {@link org.apache.lucene.document.Field}s resulting from the * mapping of the specified {@link Columns}. * * @param document the {@link Document} where the fields are going to be added * @param columns the columns */ public abstract void addFields(Document document, Columns columns); /** * Validates the specified {@link Columns} if {#validated}. * * @param columns the columns to be validated */ public final void validate(Columns columns) { if (validated) { addFields(new Document(), columns); } } /** * Returns the {@link SortField} resulting from the mapping of the specified object. * * @param name the name of the sorting field * @param reverse {@code true} the sort must be reversed, {@code false} otherwise * @return the sort field */ public abstract SortField sortField(String name, boolean reverse); /** * Returns if the specified Cassandra type/marshaller is supported. * * @param type a Cassandra type/marshaller * @return {@code true} if {@code type}, {@code false} otherwise. */ protected boolean supports(final AbstractType<?> type) { AbstractType<?> checkedType = type; if (type.isCollection()) { if (type instanceof MapType<?, ?>) { checkedType = ((MapType<?, ?>) type).getValuesType(); } else if (type instanceof ListType<?>) { checkedType = ((ListType<?>) type).getElementsType(); } else if (type instanceof SetType) { checkedType = ((SetType<?>) type).getElementsType(); } return supports(checkedType); } if (type instanceof ReversedType) { ReversedType<?> reversedType = (ReversedType<?>) type; checkedType = reversedType.baseType; } for (AbstractType<?> n : supportedTypes) { if (checkedType.getClass() == n.getClass()) { return true; } } return false; } /** * Validates this {@link Mapper} against the specified {@link CFMetaData}. * * @param metadata the column family metadata */ public final void validate(CFMetaData metadata) { for (String column : mappedColumns) { validate(metadata, column); } } /** * Finds the child {@link AbstractType} by its name. * * @param parent the parent type * @param childName the name of the child type * @return the child type, or {@code null} if it doesn't exist */ private AbstractType<?> findChildType(AbstractType<?> parent, String childName) { if (parent instanceof UserType) { UserType userType = (UserType) parent; for (int i = 0; i < userType.fieldNames().size(); i++) { if (userType.fieldNameAsString(i).equals(childName)) { return userType.fieldType(i); } } } else if (parent instanceof TupleType) { TupleType tupleType = (TupleType) parent; for (Integer i = 0; i < tupleType.size(); i++) { if (i.toString().equals(childName)) { return tupleType.type(i); } } } else if (parent.isCollection()) { CollectionType<?> collType = (CollectionType<?>) parent; switch (collType.kind) { case SET: return findChildType(collType.nameComparator(), childName); case LIST: return findChildType(collType.valueComparator(), childName); case MAP: return findChildType(collType.valueComparator(), childName); default: break; } } return null; } /** * Validates this {@link Mapper} against the specified tuple type column. * * @param metadata the column family metadata * @param column the name of the tuple column to be validated */ private void validateTuple(CFMetaData metadata, String column) { String[] names = column.split(Column.UDT_PATTERN); int numMatches = names.length; ByteBuffer parentColName = UTF8Type.instance.decompose(names[0]); ColumnDefinition parentCD = metadata.getColumnDefinition(parentColName); if (parentCD == null) { throw new IndexException("No column definition '%s' for mapper '%s'", names[0], field); } if (parentCD.isStatic()) { throw new IndexException("Lucene indexes are not allowed on static columns as '%s'", column); } AbstractType<?> actualType = parentCD.type; String columnIterator = names[0]; for (int i = 1; i < names.length; i++) { columnIterator += Column.UDT_SEPARATOR + names[i]; actualType = findChildType(actualType, names[i]); if (actualType == null) { throw new IndexException("No column definition '%s' for mapper '%s'", columnIterator, field); } if (i == (numMatches - 1)) { validate(actualType, columnIterator); } } } /** * Validates this {@link Mapper} against the specified column. * * @param metadata the column family metadata * @param column the name of the column to be validated */ private void validate(CFMetaData metadata, String column) { if (Column.isTuple(column)) { validateTuple(metadata, column); } else { ByteBuffer columnName = UTF8Type.instance.decompose(column); ColumnDefinition columnDefinition = metadata.getColumnDefinition(columnName); if (columnDefinition == null) { throw new IndexException("No column definition '%s' for mapper '%s'", column, field); } validate(columnDefinition, column); } } private void validate(ColumnDefinition columnDefinition, String column) { if (columnDefinition.isStatic()) { throw new IndexException("Lucene indexes are not allowed on static columns as '%s'", column); } validate(columnDefinition.type, column); } private void validate(AbstractType<?> type, String column) { // Check type if (!supports(type)) { throw new IndexException("'%s' is not supported by mapper '%s'", type, field); } // Avoid sorting in lists and sets if (type.isCollection() && sorted) { Kind kind = ((CollectionType<?>) type).kind; if (kind == SET) { throw new IndexException("'%s' can't be sorted because it's a set", column); } else if (kind == LIST) { throw new IndexException("'%s' can't be sorted because it's a list", column); } } } /** * Returns if this maps the specified column definition. * * @param column the column definition * @return {@code true} if this maps the column, {@code false} otherwise */ public boolean maps(ColumnDefinition column) { String name = column.name.toString(); return mappedColumns.stream().anyMatch(x -> x.equals(name)); } protected MoreObjects.ToStringHelper toStringHelper(Object self) { return MoreObjects.toStringHelper(self) .add("field", field) .add("indexed", indexed) .add("sorted", sorted) .add("validated", validated); } /** {@inheritDoc} */ @Override public String toString() { return toStringHelper(this).toString(); } }