Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Document.java
blob749e711d7ba22b0c43085f546221ce23378377a9
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.Serializable;
11 import java.util.ArrayList;
12 import java.util.Collections;
13 import java.util.HashMap;
14 import java.util.HashSet;
15 import java.util.List;
16 import java.util.Locale;
17 import java.util.Map;
18 import java.util.Set;
20 /**
21 * Represents a user generated document. The following example shows how to
22 * create a document consisting of a set of fields, some with plain text and
23 * some in HTML.
24 * <pre>
25 * Document document = Document.newBuilder().setId("document id")
26 * .setLocale(Locale.UK)
27 * .addField(Field.newBuilder()
28 * .setName("subject")
29 * .setText("going for dinner"))
30 * .addField(Field.newBuilder()
31 * .setName("body")
32 * .setHTML("&lt;html&gt;I found a restaurant.&lt;/html&gt;"))
33 * .addField(Field.newBuilder()
34 * .setName("signature")
35 * .setText("ten post jest przeznaczony dla odbiorcy")
36 * .setLocale(new Locale("pl")))
37 * .addField(Field.newBuilder()
38 * .setName("tag")
39 * .setText("food"))
40 * .addField(Field.newBuilder()
41 * .setName("tag")
42 * .setText("friend"))
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 public class Document implements Serializable {
62 static final int MAX_FIELDS_TO_STRING = 10;
64 /**
65 * A builder of documents. This is not thread-safe.
67 public static class Builder {
68 private final Map<String, List<Field>> fieldMap = new HashMap<String, List<Field>>();
69 private final List<Field> fields = new ArrayList<Field>();
71 private final Set<String> noRepeatFields = new HashSet<String>();
72 private String documentId; private Locale locale; private Integer rank;
74 /**
75 * Constructs a builder for a document.
77 protected Builder() {
80 /**
81 * Set the document id to a unique valid value. A valid document id must
82 * be a printable ASCII string of between 1 and
83 * {@literal DocumentChecker#MAXIMUM_DOCUMENT_ID_LENGTH} characters, and
84 * also not start with '!' which is reserved. If no document id is
85 * provided, then the search service will provide one when the document
86 * is indexed.
88 * @param documentId the unique id for the document to be built
89 * @return this builder
90 * @throws IllegalArgumentException if documentId is not valid
92 public Builder setId(String documentId) {
93 if (documentId != null) {
94 this.documentId = DocumentChecker.checkDocumentId(documentId);
96 return this;
99 /**
100 * Adds the field builder to the document builder. Allows multiple
101 * fields with the same name.
103 * @param builder the builder of the field to add
104 * @return this document builder
106 public Builder addField(Field.Builder builder) {
107 Preconditions.checkNotNull(builder, "field builder cannot be null");
108 return addField(builder.build());
112 * Adds the field to the builder. Allows multiple
113 * fields with the same name.
115 * @param field the field to add
116 * @return this builder
117 * @throws IllegalArgumentException if the field is invalid
119 public Builder addField(Field field) {
120 Preconditions.checkNotNull(field, "field cannot be null");
121 if (field.getType() == Field.FieldType.DATE || field.getType() == Field.FieldType.NUMBER) {
122 Preconditions.checkArgument(!noRepeatFields.contains(field.getName()),
123 "Number and date fields cannot be repeated.");
124 noRepeatFields.add(field.getName());
127 fields.add(field);
128 List<Field> fieldsForName = fieldMap.get(field.getName());
129 if (fieldsForName == null) {
130 fieldsForName = new ArrayList<Field>();
131 fieldMap.put(field.getName(), fieldsForName);
133 fieldsForName.add(field);
134 return this;
138 * Sets the {@link Locale} the document is written in.
140 * @param locale the {@link Locale} the document is written in
141 * @return this document builder
143 public Builder setLocale(Locale locale) {
144 this.locale = locale;
145 return this;
149 * Sets the rank of this document, which determines the order of documents
150 * returned by search, if no sorting or scoring is given. If it is not
151 * specified, then the number of seconds since 2011/1/1 will be used.
153 * @param rank the rank of this document
154 * @return this builder
156 public Builder setRank(int rank) {
157 this.rank = rank;
158 return this;
162 * Builds a valid document. The builder must have set a valid document
163 * id, and have a non-empty set of valid fields.
165 * @return the document built by this builder
166 * @throws IllegalArgumentException if the document built is not valid
168 public Document build() {
169 return new Document(this);
173 private static final long serialVersionUID = 309382038422977263L;
175 private final String documentId;
176 private final Map<String, List<Field>> fieldMap;
177 private final List<Field> fields;
179 private final int rank;
181 private final Locale locale;
184 * Constructs a document with the given builder.
186 * @param builder the builder capable of building a document
188 protected Document(Builder builder) {
189 documentId = builder.documentId;
190 fieldMap = new HashMap<String, List<Field>>(builder.fieldMap);
191 fields = Collections.unmodifiableList(builder.fields);
192 locale = builder.locale;
193 rank = Util.defaultIfNull(builder.rank, DocumentChecker.getNumberOfSecondsSince());
194 checkValid();
198 * @return an iterable of {@link Field} in the document
200 public Iterable<Field> getFields() {
201 return fields;
205 * @return an unmodifiable {@link Set} of the field names in the document
207 public Set<String> getFieldNames() {
208 return Collections.unmodifiableSet(fieldMap.keySet());
212 * Get an iterable of all fields with the given name.
214 * @param name the name of the field name whose values are to be returned
215 * @return an unmodifiable {@link Iterable} of {@link Field} with the given name
216 * or {@code null}
218 public Iterable<Field> getFields(String name) {
219 List<Field> fieldsForName = fieldMap.get(name);
220 if (fieldsForName == null) {
221 return null;
223 return Collections.unmodifiableList(fieldsForName);
227 * Returns the single field with the given name.
229 * @param name the name of the field to return
230 * @return the single field with name
231 * @throws IllegalArgumentException if the document does not have exactly
232 * one field with the name
234 public Field getOnlyField(String name) {
235 List<Field> fieldsForName = fieldMap.get(name);
236 Preconditions.checkArgument(
237 fieldsForName != null && fieldsForName.size() == 1,
238 "Field %s is present %d times; expected 1",
239 name, (fieldsForName == null? 0 : fieldsForName.size()));
240 return fieldsForName.get(0);
244 * Returns the number of times a field with the given name is present
245 * in this document.
247 * @param name the name of the field to be counted
248 * @return the number of times a field with the given name is present
250 public int getFieldCount(String name) {
251 List<Field> fieldsForName = fieldMap.get(name);
252 return fieldsForName == null ? 0 : fieldsForName.size();
256 * @return the id of the document
258 public String getId() {
259 return documentId;
263 * @return the {@link Locale} the document is written in. Can be null
265 public Locale getLocale() {
266 return locale;
270 * Returns the rank of this document. A document's rank is used to
271 * determine the default order in which documents are returned by
272 * search, if no sorting or scoring is specified.
274 * @return the rank of this document
276 public int getRank() {
277 return rank;
280 @Override
281 public int hashCode() {
282 return documentId.hashCode();
285 @Override
286 public boolean equals(Object object) {
287 if (this == object) {
288 return true;
290 if (!(object instanceof Document)) {
291 return false;
293 Document doc = (Document) object;
294 return documentId.equals(doc.getId());
298 * Checks whether the document is valid. A document is valid if
299 * it has a valid document id, a locale, a non-empty collection of
300 * valid fields.
302 * @return this document
303 * @throws IllegalArgumentException if the document has an invalid
304 * document id, has no fields, or some field is invalid
306 private Document checkValid() {
307 if (documentId != null) {
308 DocumentChecker.checkDocumentId(documentId);
310 Preconditions.checkArgument(fieldMap != null,
311 "Null map of fields in document for indexing");
312 Preconditions.checkArgument(fields != null,
313 "Null list of fields in document for indexing");
314 return this;
318 * Creates a new document builder. You must use this method to obtain a new
319 * builder. The returned builder must be used to specify all properties of
320 * the document. To obtain the document call the {@link Builder#build()}
321 * method on the returned builder.
323 * @return a builder which constructs a document object
325 public static Builder newBuilder() {
326 return new Builder();
330 * Creates a new document builder from the given document. All document
331 * properties are copied to the returned builder.
333 * @param document the document protocol buffer to build a document object
334 * from
335 * @return the document builder initialized from a document protocol buffer
336 * @throws SearchException if a field value is invalid
338 static Builder newBuilder(DocumentPb.Document document) {
339 Document.Builder docBuilder = Document.newBuilder().setId(document.getId());
340 if (document.hasLanguage()) {
341 docBuilder.setLocale(FieldChecker.parseLocale(document.getLanguage()));
343 for (DocumentPb.Field field : document.getFieldList()) {
344 docBuilder.addField(Field.newBuilder(field));
346 if (document.hasOrderId()) {
347 docBuilder.setRank(document.getOrderId());
349 return docBuilder;
353 * Copies a {@link Document} object into a
354 * {@link com.google.apphosting.api.search.DocumentPb.Document} protocol buffer.
356 * @return the document protocol buffer copy of the document object
357 * @throws IllegalArgumentException if any parts of the document are invalid
358 * or the document protocol buffer is too large
360 DocumentPb.Document copyToProtocolBuffer() {
361 DocumentPb.Document.Builder docBuilder = DocumentPb.Document.newBuilder();
362 if (documentId != null) {
363 docBuilder.setId(documentId);
365 if (locale != null) {
366 docBuilder.setLanguage(locale.toString());
368 for (Field field : fields) {
369 docBuilder.addField(field.copyToProtocolBuffer());
371 docBuilder.setOrderId(rank);
372 return DocumentChecker.checkValid(docBuilder.build());
375 @Override
376 public String toString() {
377 return new Util.ToStringHelper("Document")
378 .addField("documentId", documentId)
379 .addIterableField("fields", fields, MAX_FIELDS_TO_STRING)
380 .addField("locale", locale)
381 .addField("rank", rank)
382 .finish();
385 boolean isIdenticalTo(Document other) {
386 if (documentId == null) {
387 if (other.documentId != null) {
388 return false;
390 } else {
391 if (!documentId.equals(other.documentId)) {
392 return false;
395 if (fields == null) {
396 if (other.fields != null) {
397 return false;
399 } else {
400 if (!fields.equals(other.fields)) {
401 return false;
404 if (locale == null) {
405 if (other.locale != null) {
406 return false;
408 } else {
409 if (!locale.equals(other.locale)) {
410 return false;
413 return rank == other.rank;