Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Document.java
blob01d34bbb76c8aacd0c8f89b8637955aa7b3b277b
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;
9 import com.google.common.collect.HashMultimap;
10 import com.google.common.collect.SetMultimap;
12 import java.io.IOException;
13 import java.io.ObjectInputStream;
14 import java.io.Serializable;
15 import java.util.ArrayList;
16 import java.util.Collections;
17 import java.util.HashMap;
18 import java.util.LinkedHashMap;
19 import java.util.List;
20 import java.util.Locale;
21 import java.util.Map;
22 import java.util.Set;
24 /**
25 * Represents a user generated document. The following example shows how to
26 * create a document consisting of a set of fields, some with plain text and
27 * some in HTML; it also adds facets to the document.
28 * <pre>
29 * Document document = Document.newBuilder().setId("document id")
30 * .setLocale(Locale.UK)
31 * .addField(Field.newBuilder()
32 * .setName("subject")
33 * .setText("going for dinner"))
34 * .addField(Field.newBuilder()
35 * .setName("body")
36 * .setHTML("&lt;html&gt;I found a restaurant.&lt;/html&gt;"))
37 * .addField(Field.newBuilder()
38 * .setName("signature")
39 * .setText("ten post jest przeznaczony dla odbiorcy")
40 * .setLocale(new Locale("pl")))
41 * .addFacet(Facet.withAtom("tag", "food"))
42 * .addFacet(Facet.withNumber("priority", 5.0))
43 * .build();
44 * </pre>
46 * The following example shows how to access the fields within a document:
47 * <pre>
48 * Document document = ...
50 * for (Field field : document.getFields()) {
51 * switch (field.getType()) {
52 * case TEXT: use(field.getText()); break;
53 * case HTML: use(field.getHtml()); break;
54 * case ATOM: use(field.getAtom()); break;
55 * case DATE: use(field.getDate()); break;
56 * }
57 * }
58 * </pre>
60 * And this example shows how to access the facets within a document:
61 * <pre>
62 * Document document = ...
64 * for (Facet facet : document.getFacets()) {
65 * switch (facet.getType()) {
66 * case ATOM: use(facet.getAtom()); break;
67 * case NUMBER: use(facet.getNumber()); break;
68 * }
69 * }
70 * </pre>
72 public class Document implements Serializable {
74 static final int MAX_FIELDS_TO_STRING = 10;
75 static final int MAX_FACETS_TO_STRING = 10;
77 /**
78 * A builder of documents. This is not thread-safe.
80 public static class Builder {
81 private final Map<String, List<Field>> fieldMap = new HashMap<>();
82 private final List<Field> fields = new ArrayList<>();
83 private final List<Facet> facets = new ArrayList<>();
85 private final SetMultimap<String, Field.FieldType> noRepeatNames = HashMultimap.create();
86 private String documentId; private Locale locale; private Integer rank;
88 /**
89 * Constructs a builder for a document.
91 protected Builder() {
94 /**
95 * Set the document id to a unique valid value. A valid document id must
96 * be a printable ASCII string of between 1 and
97 * {@literal DocumentChecker#MAXIMUM_DOCUMENT_ID_LENGTH} characters, and
98 * also not start with '!' which is reserved. If no document id is
99 * provided, then the search service will provide one when the document
100 * is indexed.
102 * @param documentId the unique id for the document to be built
103 * @return this builder
104 * @throws IllegalArgumentException if documentId is not valid
106 public Builder setId(String documentId) {
107 if (documentId != null) {
108 this.documentId = DocumentChecker.checkDocumentId(documentId);
110 return this;
114 * Adds the field builder to the document builder. Allows multiple
115 * fields with the same name.
117 * @param builder the builder of the field to add
118 * @return this document builder
120 public Builder addField(Field.Builder builder) {
121 Preconditions.checkNotNull(builder, "field builder cannot be null");
122 return addField(builder.build());
126 * Adds the field to the builder. Allows multiple fields with the same name, except
127 * that documents may only have one date and one number field for a name.
129 * @param field the field to add
130 * @return this builder
131 * @throws IllegalArgumentException if the field is invalid
133 public Builder addField(Field field) {
134 Preconditions.checkNotNull(field, "field cannot be null");
135 Field.FieldType type = field.getType();
136 if (type == Field.FieldType.DATE || type == Field.FieldType.NUMBER) {
137 if (!noRepeatNames.put(field.getName(), type)) {
138 throw new IllegalArgumentException("Number and date fields cannot be repeated.");
142 fields.add(field);
143 List<Field> fieldsForName = fieldMap.get(field.getName());
144 if (fieldsForName == null) {
145 fieldsForName = new ArrayList<>();
146 fieldMap.put(field.getName(), fieldsForName);
148 fieldsForName.add(field);
149 return this;
153 * Adds a {@link Facet} to this builder.
155 * @param facet the facet to add
156 * @return this builder
158 public Builder addFacet(Facet facet) {
159 Preconditions.checkNotNull(facet, "facet cannot be null");
160 facets.add(facet);
161 return this;
165 * Sets the {@link Locale} the document is written in.
167 * @param locale the {@link Locale} the document is written in
168 * @return this document builder
170 public Builder setLocale(Locale locale) {
171 this.locale = locale;
172 return this;
176 * Sets the rank of this document, which determines the order of documents
177 * returned by search, if no sorting or scoring is given. If it is not
178 * specified, then the number of seconds since 2011/1/1 will be used.
180 * @param rank the rank of this document
181 * @return this builder
183 public Builder setRank(int rank) {
184 this.rank = rank;
185 return this;
189 * Builds a valid document. The builder must have set a valid document
190 * id, and have a non-empty set of valid fields.
192 * @return the document built by this builder
193 * @throws IllegalArgumentException if the document built is not valid
195 public Document build() {
196 return new Document(this);
200 private static final long serialVersionUID = 309382038422977263L;
202 private final String documentId;
203 private final Map<String, List<Field>> fieldMap;
204 private final List<Field> fields;
205 private volatile List<Facet> facets;
206 private transient volatile Map<String, List<Facet>> facetMap;
208 private final int rank;
210 private final Locale locale;
213 * Constructs a document with the given builder.
215 * @param builder the builder capable of building a document
217 protected Document(Builder builder) {
218 documentId = builder.documentId;
219 fieldMap = new HashMap<>(builder.fieldMap);
220 fields = Collections.unmodifiableList(builder.fields);
221 facets = Collections.unmodifiableList(builder.facets);
222 facetMap = buildFacetMap(facets);
223 locale = builder.locale;
224 rank = Util.defaultIfNull(builder.rank, DocumentChecker.getNumberOfSecondsSince());
225 checkValid();
229 * Returns an iterable of {@link Field} in the document
231 public Iterable<Field> getFields() {
232 return fields;
236 * Returns an iterable of {@link Facet} in the document
238 public Iterable<Facet> getFacets() {
239 return facets;
243 * Returns an unmodifiable {@link Set} of the field names in the document
245 public Set<String> getFieldNames() {
246 return Collections.unmodifiableSet(fieldMap.keySet());
250 * Returns an unmodifiable {@link Set} of the facet names in the document
252 public Set<String> getFacetNames() {
253 return facetMap.keySet();
257 * Returns an iterable of all fields with the given name.
259 * @param name the name of the field whose values are to be returned
260 * @return an unmodifiable {@link Iterable} of {@link Field} with the given name
261 * or {@code null}
263 public Iterable<Field> getFields(String name) {
264 List<Field> fieldsForName = fieldMap.get(name);
265 if (fieldsForName == null) {
266 return null;
268 return Collections.unmodifiableList(fieldsForName);
272 * Returns an iterable of all facets with the given name.
274 * @param name the name of the facet whose values are to be returned
275 * @return an unmodifiable {@link Iterable} of {@link Facet} with the given name
276 * or {@code null}
278 public Iterable<Facet> getFacets(String name) {
279 List<Facet> facetsForName = facetMap.get(name);
280 if (facetsForName == null) {
281 return null;
283 return Collections.unmodifiableList(facetsForName);
287 * Returns the single field with the given name.
289 * @param name the name of the field to return
290 * @return the single field with name
291 * @throws IllegalArgumentException if the document does not have exactly
292 * one field with the name
294 public Field getOnlyField(String name) {
295 List<Field> fieldsForName = fieldMap.get(name);
296 Preconditions.checkArgument(
297 fieldsForName != null && fieldsForName.size() == 1,
298 "Field %s is present %d times; expected 1",
299 name, (fieldsForName == null ? 0 : fieldsForName.size()));
300 return fieldsForName.get(0);
304 * Returns the single facet with the given name.
306 * @param name the name of the facet to return
307 * @return the single facet with name
308 * @throws IllegalArgumentException if the document does not have exactly
309 * one facet with the name
311 public Facet getOnlyFacet(String name) {
312 List<Facet> facetsForName = facetMap.get(name);
313 Preconditions.checkArgument(
314 facetsForName != null && facetsForName.size() == 1,
315 "Facet %s is present %d times; expected 1",
316 name, (facetsForName == null ? 0 : facetsForName.size()));
317 return facetsForName.get(0);
321 * Returns the number of times a field with the given name is present
322 * in this document.
324 * @param name the name of the field to be counted
325 * @return the number of times a field with the given name is present
327 public int getFieldCount(String name) {
328 List<Field> fieldsForName = fieldMap.get(name);
329 return fieldsForName == null ? 0 : fieldsForName.size();
333 * Returns the number of times a facet with the given name is present
334 * in this document.
336 * @param name the name of the facet to be counted
337 * @return the number of times a facet with the given name is present
339 public int getFacetCount(String name) {
340 List<Facet> facetsForName = facetMap.get(name);
341 return facetsForName == null ? 0 : facetsForName.size();
345 * @return the id of the document
347 public String getId() {
348 return documentId;
352 * @return the {@link Locale} the document is written in. Can be null
354 public Locale getLocale() {
355 return locale;
359 * Returns the rank of this document. A document's rank is used to
360 * determine the default order in which documents are returned by
361 * search, if no sorting or scoring is specified.
363 * @return the rank of this document
365 public int getRank() {
366 return rank;
369 @Override
370 public int hashCode() {
371 return documentId.hashCode();
374 @Override
375 public boolean equals(Object object) {
376 if (this == object) {
377 return true;
379 if (!(object instanceof Document)) {
380 return false;
382 Document doc = (Document) object;
383 return documentId.equals(doc.getId());
387 * Checks whether the document is valid. A document is valid if
388 * it has a valid document id, a locale, a non-empty collection of
389 * valid fields.
391 * @return this document
392 * @throws IllegalArgumentException if the document has an invalid
393 * document id, has no fields, or some field is invalid
395 private Document checkValid() {
396 if (documentId != null) {
397 DocumentChecker.checkDocumentId(documentId);
399 Preconditions.checkArgument(fieldMap != null,
400 "Null map of fields in document for indexing");
401 Preconditions.checkArgument(fields != null,
402 "Null list of fields in document for indexing");
403 Preconditions.checkArgument(facetMap != null,
404 "Null map of facets in document for indexing");
405 Preconditions.checkArgument(facets != null,
406 "Null list of facets in document for indexing");
407 return this;
410 private static Map<String, List<Facet>> buildFacetMap(List<Facet> facets) {
411 Map<String, List<Facet>> facetMap = new LinkedHashMap<>();
412 for (Facet facet : facets) {
413 List<Facet> facetsForName = facetMap.get(facet.getName());
414 if (facetsForName == null) {
415 facetsForName = new ArrayList<>();
416 facetMap.put(facet.getName(), facetsForName);
418 facetsForName.add(facet);
420 return Collections.unmodifiableMap(facetMap);
423 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
424 in.defaultReadObject();
425 if (facets == null) {
426 facets = Collections.emptyList();
428 facetMap = buildFacetMap(facets);
432 * Creates a new document builder. You must use this method to obtain a new
433 * builder. The returned builder must be used to specify all properties of
434 * the document. To obtain the document call the {@link Builder#build()}
435 * method on the returned builder.
437 * @return a builder which constructs a document object
439 public static Builder newBuilder() {
440 return new Builder();
444 * Creates a new document builder from the given document. All document
445 * properties are copied to the returned builder.
447 * @param document the document protocol buffer to build a document object
448 * from
449 * @return the document builder initialized from a document protocol buffer
450 * @throws SearchException if a field or facet value is invalid
452 static Builder newBuilder(DocumentPb.Document document) {
453 Document.Builder docBuilder = Document.newBuilder().setId(document.getId());
454 if (document.hasLanguage()) {
455 docBuilder.setLocale(FieldChecker.parseLocale(document.getLanguage()));
457 for (DocumentPb.Field field : document.getFieldList()) {
458 docBuilder.addField(Field.newBuilder(field));
460 for (DocumentPb.Facet facet : document.getFacetList()) {
461 docBuilder.addFacet(Facet.withProtoMessage(facet));
463 if (document.hasOrderId()) {
464 docBuilder.setRank(document.getOrderId());
466 return docBuilder;
470 * Copies a {@link Document} object into a
471 * {@link com.google.apphosting.api.search.DocumentPb.Document} protocol buffer.
473 * @return the document protocol buffer copy of the document object
474 * @throws IllegalArgumentException if any parts of the document are invalid
475 * or the document protocol buffer is too large
477 DocumentPb.Document copyToProtocolBuffer() {
478 DocumentPb.Document.Builder docBuilder = DocumentPb.Document.newBuilder();
479 if (documentId != null) {
480 docBuilder.setId(documentId);
482 if (locale != null) {
483 docBuilder.setLanguage(locale.toString());
485 for (Field field : fields) {
486 docBuilder.addField(field.copyToProtocolBuffer());
488 for (Facet facet : getFacets()) {
489 docBuilder.addFacet(facet.copyToProtocolBuffer());
491 docBuilder.setOrderId(rank);
492 return DocumentChecker.checkValid(docBuilder.build());
495 @Override
496 public String toString() {
497 return new Util.ToStringHelper("Document")
498 .addField("documentId", documentId)
499 .addIterableField("fields", fields, MAX_FIELDS_TO_STRING)
500 .addIterableField("facets", getFacets(), MAX_FACETS_TO_STRING)
501 .addField("locale", locale)
502 .addField("rank", rank)
503 .finish();
506 boolean isIdenticalTo(Document other) {
507 if (documentId == null) {
508 if (other.documentId != null) {
509 return false;
511 } else {
512 if (!documentId.equals(other.documentId)) {
513 return false;
516 if (fields == null) {
517 if (other.fields != null) {
518 return false;
520 } else {
521 if (!fields.equals(other.fields)) {
522 return false;
525 if (!getFacets().equals(other.getFacets())) {
526 return false;
528 if (locale == null) {
529 if (other.locale != null) {
530 return false;
532 } else {
533 if (!locale.equals(other.locale)) {
534 return false;
537 return rank == other.rank;