Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Document.java
blob8844a78014bd5b09e7b6d4d44964f2d4f7d4c667
1 // Copyright 2010 Google Inc. All Rights Reserved.
3 package com.google.appengine.api.search;
5 import com.google.appengine.api.search.checkers.DocumentChecker;
6 import com.google.appengine.api.search.checkers.FieldChecker;
7 import com.google.appengine.api.search.checkers.Preconditions;
8 import com.google.apphosting.api.search.DocumentPb;
10 import java.io.IOException;
11 import java.io.ObjectInputStream;
12 import java.io.Serializable;
13 import java.util.ArrayList;
14 import java.util.Collections;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.LinkedHashMap;
18 import java.util.List;
19 import java.util.Locale;
20 import java.util.Map;
21 import java.util.Set;
23 /**
24 * Represents a user generated document. The following example shows how to
25 * create a document consisting of a set of fields, some with plain text and
26 * some in HTML; it also adds facets to the document.
27 * <pre>
28 * Document document = Document.newBuilder().setId("document id")
29 * .setLocale(Locale.UK)
30 * .addField(Field.newBuilder()
31 * .setName("subject")
32 * .setText("going for dinner"))
33 * .addField(Field.newBuilder()
34 * .setName("body")
35 * .setHTML("&lt;html&gt;I found a restaurant.&lt;/html&gt;"))
36 * .addField(Field.newBuilder()
37 * .setName("signature")
38 * .setText("ten post jest przeznaczony dla odbiorcy")
39 * .setLocale(new Locale("pl")))
40 * .addFacet(Facet.withAtom("tag", "food"))
41 * .addFacet(Facet.withAtom("tag", "friend"))
42 * .build();
43 * </pre>
45 * The following example shows how to access the fields within a document:
46 * <pre>
47 * Document document = ...
49 * for (Field field : document.getFields()) {
50 * switch (field.getType()) {
51 * case TEXT: use(field.getText()); break;
52 * case HTML: use(field.getHtml()); break;
53 * case ATOM: use(field.getAtom()); break;
54 * case DATE: use(field.getDate()); break;
55 * }
56 * }
57 * </pre>
59 * And this example shows how to access the facets within a document:
60 * <pre>
61 * Document document = ...
63 * for (Facet facet : document.getFacets()) {
64 * switch (facet.getType()) {
65 * case ATOM: use(facet.getAtom()); break;
66 * case NUMBER: use(facet.getNumber()); break;
67 * }
68 * }
69 * </pre>
71 public class Document implements Serializable {
73 static final int MAX_FIELDS_TO_STRING = 10;
74 static final int MAX_FACETS_TO_STRING = 10;
76 /**
77 * A builder of documents. This is not thread-safe.
79 public static class Builder {
80 private final Map<String, List<Field>> fieldMap = new HashMap<>();
81 private final List<Field> fields = new ArrayList<>();
82 private final List<Facet> facets = new ArrayList<>();
84 private final Set<String> noRepeatFields = new HashSet<>();
85 private String documentId; private Locale locale; private Integer rank;
87 /**
88 * Constructs a builder for a document.
90 protected Builder() {
93 /**
94 * Set the document id to a unique valid value. A valid document id must
95 * be a printable ASCII string of between 1 and
96 * {@literal DocumentChecker#MAXIMUM_DOCUMENT_ID_LENGTH} characters, and
97 * also not start with '!' which is reserved. If no document id is
98 * provided, then the search service will provide one when the document
99 * is indexed.
101 * @param documentId the unique id for the document to be built
102 * @return this builder
103 * @throws IllegalArgumentException if documentId is not valid
105 public Builder setId(String documentId) {
106 if (documentId != null) {
107 this.documentId = DocumentChecker.checkDocumentId(documentId);
109 return this;
113 * Adds the field builder to the document builder. Allows multiple
114 * fields with the same name.
116 * @param builder the builder of the field to add
117 * @return this document builder
119 public Builder addField(Field.Builder builder) {
120 Preconditions.checkNotNull(builder, "field builder cannot be null");
121 return addField(builder.build());
125 * Adds the field to the builder. Allows multiple
126 * fields with the same name.
128 * @param field the field to add
129 * @return this builder
130 * @throws IllegalArgumentException if the field is invalid
132 public Builder addField(Field field) {
133 Preconditions.checkNotNull(field, "field cannot be null");
134 if (field.getType() == Field.FieldType.DATE || field.getType() == Field.FieldType.NUMBER) {
135 Preconditions.checkArgument(!noRepeatFields.contains(field.getName()),
136 "Number and date fields cannot be repeated.");
137 noRepeatFields.add(field.getName());
140 fields.add(field);
141 List<Field> fieldsForName = fieldMap.get(field.getName());
142 if (fieldsForName == null) {
143 fieldsForName = new ArrayList<>();
144 fieldMap.put(field.getName(), fieldsForName);
146 fieldsForName.add(field);
147 return this;
151 * Adds a {@link Facet} to this builder.
153 * @param facet the facet to add
154 * @return this builder
156 public Builder addFacet(Facet facet) {
157 Preconditions.checkNotNull(facet, "facet cannot be null");
158 facets.add(facet);
159 return this;
163 * Sets the {@link Locale} the document is written in.
165 * @param locale the {@link Locale} the document is written in
166 * @return this document builder
168 public Builder setLocale(Locale locale) {
169 this.locale = locale;
170 return this;
174 * Sets the rank of this document, which determines the order of documents
175 * returned by search, if no sorting or scoring is given. If it is not
176 * specified, then the number of seconds since 2011/1/1 will be used.
178 * @param rank the rank of this document
179 * @return this builder
181 public Builder setRank(int rank) {
182 this.rank = rank;
183 return this;
187 * Builds a valid document. The builder must have set a valid document
188 * id, and have a non-empty set of valid fields.
190 * @return the document built by this builder
191 * @throws IllegalArgumentException if the document built is not valid
193 public Document build() {
194 return new Document(this);
198 private static final long serialVersionUID = 309382038422977263L;
200 private final String documentId;
201 private final Map<String, List<Field>> fieldMap;
202 private final List<Field> fields;
203 private volatile List<Facet> facets;
204 private transient volatile Map<String, List<Facet>> facetMap;
206 private final int rank;
208 private final Locale locale;
211 * Constructs a document with the given builder.
213 * @param builder the builder capable of building a document
215 protected Document(Builder builder) {
216 documentId = builder.documentId;
217 fieldMap = new HashMap<>(builder.fieldMap);
218 fields = Collections.unmodifiableList(builder.fields);
219 facets = Collections.unmodifiableList(builder.facets);
220 facetMap = buildFacetMap(facets);
221 locale = builder.locale;
222 rank = Util.defaultIfNull(builder.rank, DocumentChecker.getNumberOfSecondsSince());
223 checkValid();
227 * Returns an iterable of {@link Field} in the document
229 public Iterable<Field> getFields() {
230 return fields;
234 * Returns an iterable of {@link Facet} in the document
236 public Iterable<Facet> getFacets() {
237 return facets;
241 * Returns an unmodifiable {@link Set} of the field names in the document
243 public Set<String> getFieldNames() {
244 return Collections.unmodifiableSet(fieldMap.keySet());
248 * Returns an unmodifiable {@link Set} of the facet names in the document
250 public Set<String> getFacetNames() {
251 return facetMap.keySet();
255 * Returns an iterable of all fields with the given name.
257 * @param name the name of the field whose values are to be returned
258 * @return an unmodifiable {@link Iterable} of {@link Field} with the given name
259 * or {@code null}
261 public Iterable<Field> getFields(String name) {
262 List<Field> fieldsForName = fieldMap.get(name);
263 if (fieldsForName == null) {
264 return null;
266 return Collections.unmodifiableList(fieldsForName);
270 * Returns an iterable of all facets with the given name.
272 * @param name the name of the facet whose values are to be returned
273 * @return an unmodifiable {@link Iterable} of {@link Facet} with the given name
274 * or {@code null}
276 public Iterable<Facet> getFacets(String name) {
277 List<Facet> facetsForName = facetMap.get(name);
278 if (facetsForName == null) {
279 return null;
281 return Collections.unmodifiableList(facetsForName);
285 * Returns the single field with the given name.
287 * @param name the name of the field to return
288 * @return the single field with name
289 * @throws IllegalArgumentException if the document does not have exactly
290 * one field with the name
292 public Field getOnlyField(String name) {
293 List<Field> fieldsForName = fieldMap.get(name);
294 Preconditions.checkArgument(
295 fieldsForName != null && fieldsForName.size() == 1,
296 "Field %s is present %d times; expected 1",
297 name, (fieldsForName == null ? 0 : fieldsForName.size()));
298 return fieldsForName.get(0);
302 * Returns the single facet with the given name.
304 * @param name the name of the facet to return
305 * @return the single facet with name
306 * @throws IllegalArgumentException if the document does not have exactly
307 * one facet with the name
309 public Facet getOnlyFacet(String name) {
310 List<Facet> facetsForName = facetMap.get(name);
311 Preconditions.checkArgument(
312 facetsForName != null && facetsForName.size() == 1,
313 "Facet %s is present %d times; expected 1",
314 name, (facetsForName == null ? 0 : facetsForName.size()));
315 return facetsForName.get(0);
319 * Returns the number of times a field with the given name is present
320 * in this document.
322 * @param name the name of the field to be counted
323 * @return the number of times a field with the given name is present
325 public int getFieldCount(String name) {
326 List<Field> fieldsForName = fieldMap.get(name);
327 return fieldsForName == null ? 0 : fieldsForName.size();
331 * Returns the number of times a facet with the given name is present
332 * in this document.
334 * @param name the name of the facet to be counted
335 * @return the number of times a facet with the given name is present
337 public int getFacetCount(String name) {
338 List<Facet> facetsForName = facetMap.get(name);
339 return facetsForName == null ? 0 : facetsForName.size();
343 * @return the id of the document
345 public String getId() {
346 return documentId;
350 * @return the {@link Locale} the document is written in. Can be null
352 public Locale getLocale() {
353 return locale;
357 * Returns the rank of this document. A document's rank is used to
358 * determine the default order in which documents are returned by
359 * search, if no sorting or scoring is specified.
361 * @return the rank of this document
363 public int getRank() {
364 return rank;
367 @Override
368 public int hashCode() {
369 return documentId.hashCode();
372 @Override
373 public boolean equals(Object object) {
374 if (this == object) {
375 return true;
377 if (!(object instanceof Document)) {
378 return false;
380 Document doc = (Document) object;
381 return documentId.equals(doc.getId());
385 * Checks whether the document is valid. A document is valid if
386 * it has a valid document id, a locale, a non-empty collection of
387 * valid fields.
389 * @return this document
390 * @throws IllegalArgumentException if the document has an invalid
391 * document id, has no fields, or some field is invalid
393 private Document checkValid() {
394 if (documentId != null) {
395 DocumentChecker.checkDocumentId(documentId);
397 Preconditions.checkArgument(fieldMap != null,
398 "Null map of fields in document for indexing");
399 Preconditions.checkArgument(fields != null,
400 "Null list of fields in document for indexing");
401 Preconditions.checkArgument(facetMap != null,
402 "Null map of facets in document for indexing");
403 Preconditions.checkArgument(facets != null,
404 "Null list of facets in document for indexing");
405 return this;
408 private static Map<String, List<Facet>> buildFacetMap(List<Facet> facets) {
409 Map<String, List<Facet>> facetMap = new LinkedHashMap<>();
410 for (Facet facet : facets) {
411 List<Facet> facetsForName = facetMap.get(facet.getName());
412 if (facetsForName == null) {
413 facetsForName = new ArrayList<>();
414 facetMap.put(facet.getName(), facetsForName);
416 facetsForName.add(facet);
418 return Collections.unmodifiableMap(facetMap);
421 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
422 in.defaultReadObject();
423 if (facets == null) {
424 facets = Collections.emptyList();
426 facetMap = buildFacetMap(facets);
430 * Creates a new document builder. You must use this method to obtain a new
431 * builder. The returned builder must be used to specify all properties of
432 * the document. To obtain the document call the {@link Builder#build()}
433 * method on the returned builder.
435 * @return a builder which constructs a document object
437 public static Builder newBuilder() {
438 return new Builder();
442 * Creates a new document builder from the given document. All document
443 * properties are copied to the returned builder.
445 * @param document the document protocol buffer to build a document object
446 * from
447 * @return the document builder initialized from a document protocol buffer
448 * @throws SearchException if a field or facet value is invalid
450 static Builder newBuilder(DocumentPb.Document document) {
451 Document.Builder docBuilder = Document.newBuilder().setId(document.getId());
452 if (document.hasLanguage()) {
453 docBuilder.setLocale(FieldChecker.parseLocale(document.getLanguage()));
455 for (DocumentPb.Field field : document.getFieldList()) {
456 docBuilder.addField(Field.newBuilder(field));
458 for (DocumentPb.Facet facet : document.getFacetList()) {
459 docBuilder.addFacet(Facet.withProtoMessage(facet));
461 if (document.hasOrderId()) {
462 docBuilder.setRank(document.getOrderId());
464 return docBuilder;
468 * Copies a {@link Document} object into a
469 * {@link com.google.apphosting.api.search.DocumentPb.Document} protocol buffer.
471 * @return the document protocol buffer copy of the document object
472 * @throws IllegalArgumentException if any parts of the document are invalid
473 * or the document protocol buffer is too large
475 DocumentPb.Document copyToProtocolBuffer() {
476 DocumentPb.Document.Builder docBuilder = DocumentPb.Document.newBuilder();
477 if (documentId != null) {
478 docBuilder.setId(documentId);
480 if (locale != null) {
481 docBuilder.setLanguage(locale.toString());
483 for (Field field : fields) {
484 docBuilder.addField(field.copyToProtocolBuffer());
486 for (Facet facet : getFacets()) {
487 docBuilder.addFacet(facet.copyToProtocolBuffer());
489 docBuilder.setOrderId(rank);
490 return DocumentChecker.checkValid(docBuilder.build());
493 @Override
494 public String toString() {
495 return new Util.ToStringHelper("Document")
496 .addField("documentId", documentId)
497 .addIterableField("fields", fields, MAX_FIELDS_TO_STRING)
498 .addIterableField("facets", getFacets(), MAX_FACETS_TO_STRING)
499 .addField("locale", locale)
500 .addField("rank", rank)
501 .finish();
504 boolean isIdenticalTo(Document other) {
505 if (documentId == null) {
506 if (other.documentId != null) {
507 return false;
509 } else {
510 if (!documentId.equals(other.documentId)) {
511 return false;
514 if (fields == null) {
515 if (other.fields != null) {
516 return false;
518 } else {
519 if (!fields.equals(other.fields)) {
520 return false;
523 if (!getFacets().equals(other.getFacets())) {
524 return false;
526 if (locale == null) {
527 if (other.locale != null) {
528 return false;
530 } else {
531 if (!locale.equals(other.locale)) {
532 return false;
535 return rank == other.rank;