/* * Licensed to Elastic Search and Shay Banon under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Elastic Search 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 org.elasticsearch.index.mapper.internal; import org.apache.lucene.document.Field; import org.apache.lucene.document.Fieldable; import org.apache.lucene.index.FieldInfo.IndexOptions; import org.apache.lucene.index.Term; import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; import org.apache.lucene.search.XTermsFilter; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.*; import org.elasticsearch.index.mapper.core.AbstractFieldMapper; import org.elasticsearch.index.query.QueryParseContext; import java.io.IOException; import java.util.Map; /** * */ public class ParentFieldMapper extends AbstractFieldMapper<Uid> implements InternalMapper, RootMapper { public static final String NAME = "_parent"; public static final String CONTENT_TYPE = "_parent"; public static class Defaults extends AbstractFieldMapper.Defaults { public static final String NAME = ParentFieldMapper.NAME; public static final Field.Index INDEX = Field.Index.NOT_ANALYZED; public static final boolean OMIT_NORMS = true; public static final IndexOptions INDEX_OPTIONS = IndexOptions.DOCS_ONLY; } public static class Builder extends Mapper.Builder<Builder, ParentFieldMapper> { protected String indexName; private String type; public Builder() { super(Defaults.NAME); this.indexName = name; } public Builder type(String type) { this.type = type; return builder; } @Override public ParentFieldMapper build(BuilderContext context) { if (type == null) { throw new MapperParsingException("Parent mapping must contain the parent type"); } return new ParentFieldMapper(name, indexName, type); } } public static class TypeParser implements Mapper.TypeParser { @Override public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException { ParentFieldMapper.Builder builder = new ParentFieldMapper.Builder(); for (Map.Entry<String, Object> entry : node.entrySet()) { String fieldName = Strings.toUnderscoreCase(entry.getKey()); Object fieldNode = entry.getValue(); if (fieldName.equals("type")) { builder.type(fieldNode.toString()); } } return builder; } } private final String type; protected ParentFieldMapper(String name, String indexName, String type) { super(new Names(name, indexName, indexName, name), Defaults.INDEX, Field.Store.YES, Defaults.TERM_VECTOR, Defaults.BOOST, Defaults.OMIT_NORMS, Defaults.INDEX_OPTIONS, Lucene.KEYWORD_ANALYZER, Lucene.KEYWORD_ANALYZER); this.type = type; } public String type() { return type; } @Override public void preParse(ParseContext context) throws IOException { } @Override public void postParse(ParseContext context) throws IOException { parse(context); } @Override public void validate(ParseContext context) throws MapperParsingException { } @Override public boolean includeInObject() { return true; } @Override protected Field parseCreateField(ParseContext context) throws IOException { if (context.parser().currentName() != null && context.parser().currentName().equals(Defaults.NAME)) { // we are in the parsing of _parent phase String parentId = context.parser().text(); context.sourceToParse().parent(parentId); return new Field(names.indexName(), Uid.createUid(context.stringBuilder(), type, parentId), store, index); } // otherwise, we are running it post processing of the xcontent String parsedParentId = context.doc().get(Defaults.NAME); if (context.sourceToParse().parent() != null) { String parentId = context.sourceToParse().parent(); if (parsedParentId == null) { if (parentId == null) { throw new MapperParsingException("No parent id provided, not within the document, and not externally"); } // we did not add it in the parsing phase, add it now return new Field(names.indexName(), Uid.createUid(context.stringBuilder(), type, parentId), store, index); } else if (parentId != null && !parsedParentId.equals(Uid.createUid(context.stringBuilder(), type, parentId))) { throw new MapperParsingException("Parent id mismatch, document value is [" + Uid.createUid(parsedParentId).id() + "], while external value is [" + parentId + "]"); } } // we have parent mapping, yet no value was set, ignore it... return null; } @Override public Uid value(Fieldable field) { return Uid.createUid(field.stringValue()); } @Override public Uid valueFromString(String value) { return Uid.createUid(value); } @Override public String valueAsString(Fieldable field) { return field.stringValue(); } @Override public Object valueForSearch(Fieldable field) { String fieldValue = field.stringValue(); if (fieldValue == null) { return null; } int index = fieldValue.indexOf(Uid.DELIMITER); if (index == -1) { return fieldValue; } return fieldValue.substring(index + 1); } @Override public String indexedValue(String value) { if (value.indexOf(Uid.DELIMITER) == -1) { return Uid.createUid(type, value); } return value; } @Override public Query fieldQuery(String value, @Nullable QueryParseContext context) { if (context == null) { return super.fieldQuery(value, context); } return new ConstantScoreQuery(fieldFilter(value, context)); } @Override public Filter fieldFilter(String value, @Nullable QueryParseContext context) { if (context == null) { return super.fieldFilter(value, context); } // we use all types, cause we don't know if its exact or not... Term[] typesTerms = new Term[context.mapperService().types().size()]; int i = 0; for (String type : context.mapperService().types()) { typesTerms[i++] = names.createIndexNameTerm(Uid.createUid(type, value)); } return new XTermsFilter(typesTerms); } /** * We don't need to analyzer the text, and we need to convert it to UID... */ @Override public boolean useFieldQueryWithQueryString() { return true; } public Term term(String type, String id) { return term(Uid.createUid(type, id)); } public Term term(String uid) { return names().createIndexNameTerm(uid); } @Override protected String contentType() { return CONTENT_TYPE; } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(CONTENT_TYPE); builder.field("type", type); builder.endObject(); return builder; } @Override public void merge(Mapper mergeWith, MergeContext mergeContext) throws MergeMappingException { // do nothing here, no merging, but also no exception } }