Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Document.java
blob7454206f3e06b04e65a5592aa1d3b6a1a39b7780
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>
61 public class Document implements Serializable {
63 static final int MAX_FIELDS_TO_STRING = 10;
65 /**
66 * A builder of documents. This is not thread-safe.
68 public static class Builder {
69 private final Map<String, List<Field>> fieldMap = new HashMap<String, List<Field>>();
70 private final List<Field> fields = new ArrayList<Field>();
72 private final Set<String> noRepeatFields = new HashSet<String>();
73 private String documentId; private Locale locale; private Integer rank;
75 /**
76 * Constructs a builder for a document.
78 protected Builder() {
81 /**
82 * Set the document id to a unique valid value. A valid document id must
83 * be a printable ASCII string of between 1 and
84 * {@literal DocumentChecker#MAXIMUM_DOCUMENT_ID_LENGTH} characters, and
85 * also not start with '!' which is reserved. If no document id is
86 * provided, then the search service will provide one when the document
87 * is indexed.
89 * @param documentId the unique id for the document to be built
90 * @return this builder
91 * @throws IllegalArgumentException if documentId is not valid
93 public Builder setId(String documentId) {
94 if (documentId != null) {
95 this.documentId = DocumentChecker.checkDocumentId(documentId);
97 return this;
101 * Adds the field builder to the document builder. Allows multiple
102 * fields with the same name.
104 * @param builder the builder of the field to add
105 * @return this document builder
107 public Builder addField(Field.Builder builder) {
108 Preconditions.checkNotNull(builder, "field builder cannot be null");
109 return addField(builder.build());
113 * Adds the field to the builder. Allows multiple
114 * fields with the same name.
116 * @param field the field to add
117 * @return this builder
118 * @throws IllegalArgumentException if the field is invalid
120 public Builder addField(Field field) {
121 Preconditions.checkNotNull(field, "field cannot be null");
122 if (field.getType() == Field.FieldType.DATE || field.getType() == Field.FieldType.NUMBER) {
123 Preconditions.checkArgument(!noRepeatFields.contains(field.getName()),
124 "Number and date fields cannot be repeated.");
125 noRepeatFields.add(field.getName());
128 fields.add(field);
129 List<Field> fieldsForName = fieldMap.get(field.getName());
130 if (fieldsForName == null) {
131 fieldsForName = new ArrayList<Field>();
132 fieldMap.put(field.getName(), fieldsForName);
134 fieldsForName.add(field);
135 return this;
139 * Sets the {@link Locale} the document is written in.
141 * @param locale the {@link Locale} the document is written in
142 * @return this document builder
144 public Builder setLocale(Locale locale) {
145 this.locale = locale;
146 return this;
150 * Sets the rank of this document, which determines the order of documents
151 * returned by search, if no sorting or scoring is given. If it is not
152 * specified, then the number of seconds since 2011/1/1 will be used.
154 * @param rank the rank of this document
155 * @return this builder
157 public Builder setRank(int rank) {
158 this.rank = rank;
159 return this;
163 * Builds a valid document. The builder must have set a valid document
164 * id, and have a non-empty set of valid fields.
166 * @return the document built by this builder
167 * @throws IllegalArgumentException if the document built is not valid
169 public Document build() {
170 return new Document(this);
174 private static final long serialVersionUID = 309382038422977263L;
176 private final String documentId;
177 private final Map<String, List<Field>> fieldMap;
178 private final List<Field> fields;
180 private final int rank;
182 private final Locale locale;
185 * Constructs a document with the given builder.
187 * @param builder the builder capable of building a document
189 protected Document(Builder builder) {
190 documentId = builder.documentId;
191 fieldMap = new HashMap<String, List<Field>>(builder.fieldMap);
192 fields = Collections.unmodifiableList(builder.fields);
193 locale = builder.locale;
194 rank = Util.defaultIfNull(builder.rank, DocumentChecker.getNumberOfSecondsSince());
195 checkValid();
199 * @return an iterable of {@link Field} in the document
201 public Iterable<Field> getFields() {
202 return fields;
206 * @return an unmodifiable {@link Set} of the field names in the document
208 public Set<String> getFieldNames() {
209 return Collections.unmodifiableSet(fieldMap.keySet());
213 * Get an iterable of all fields with the given name.
215 * @param name the name of the field name whose values are to be returned
216 * @return an unmodifiable {@link Iterable} of {@link Field} with the given name
217 * or {@code null}
219 public Iterable<Field> getFields(String name) {
220 List<Field> fieldsForName = fieldMap.get(name);
221 if (fieldsForName == null) {
222 return null;
224 return Collections.unmodifiableList(fieldsForName);
228 * Returns the single field with the given name.
230 * @param name the name of the field to return
231 * @return the single field with name
232 * @throws IllegalArgumentException if the document does not have exactly
233 * one field with the name
235 public Field getOnlyField(String name) {
236 List<Field> fieldsForName = fieldMap.get(name);
237 Preconditions.checkArgument(
238 fieldsForName != null && fieldsForName.size() == 1,
239 "Field %s is present %d times; expected 1",
240 name, (fieldsForName == null? 0 : fieldsForName.size()));
241 return fieldsForName.get(0);
245 * Returns the number of times a field with the given name is present
246 * in this document.
248 * @param name the name of the field to be counted
249 * @return the number of times a field with the given name is present
251 public int getFieldCount(String name) {
252 List<Field> fieldsForName = fieldMap.get(name);
253 return fieldsForName == null ? 0 : fieldsForName.size();
257 * @return the id of the document
259 public String getId() {
260 return documentId;
264 * @return the {@link Locale} the document is written in. Can be null
266 public Locale getLocale() {
267 return locale;
271 * Returns the rank of this document. A document's rank is used to
272 * determine the default order in which documents are returned by
273 * search, if no sorting or scoring is specified.
275 * @return the rank of this document
277 public int getRank() {
278 return rank;
281 @Override
282 public int hashCode() {
283 return documentId.hashCode();
286 @Override
287 public boolean equals(Object object) {
288 if (this == object) {
289 return true;
291 if (!(object instanceof Document)) {
292 return false;
294 Document doc = (Document) object;
295 return documentId.equals(doc.getId());
299 * Checks whether the document is valid. A document is valid if
300 * it has a valid document id, a locale, a non-empty collection of
301 * valid fields.
303 * @return this document
304 * @throws IllegalArgumentException if the document has an invalid
305 * document id, has no fields, or some field is invalid
307 private Document checkValid() {
308 if (documentId != null) {
309 DocumentChecker.checkDocumentId(documentId);
311 Preconditions.checkArgument(fieldMap != null,
312 "Null map of fields in document for indexing");
313 Preconditions.checkArgument(fields != null,
314 "Null list of fields in document for indexing");
315 return this;
319 * Creates a new document builder. You must use this method to obtain a new
320 * builder. The returned builder must be used to specify all properties of
321 * the document. To obtain the document call the {@link Builder#build()}
322 * method on the returned builder.
324 * @return a builder which constructs a document object
326 public static Builder newBuilder() {
327 return new Builder();
331 * Creates a new document builder from the given document. All document
332 * properties are copied to the returned builder.
334 * @param document the document protocol buffer to build a document object
335 * from
336 * @return the document builder initialized from a document protocol buffer
337 * @throws SearchException if a field value is invalid
339 static Builder newBuilder(DocumentPb.Document document) {
340 Document.Builder docBuilder = Document.newBuilder().setId(document.getId());
341 if (document.hasLanguage()) {
342 docBuilder.setLocale(FieldChecker.parseLocale(document.getLanguage()));
344 for (DocumentPb.Field field : document.getFieldList()) {
345 docBuilder.addField(Field.newBuilder(field));
347 if (document.hasOrderId()) {
348 docBuilder.setRank(document.getOrderId());
350 return docBuilder;
354 * Copies a {@link Document} object into a {@link DocumentPb.Document}
355 * protocol buffer.
357 * @return the document protocol buffer copy of the document object
358 * @throws IllegalArgumentException if any parts of the document are invalid
359 * or the document protocol buffer is too large
361 DocumentPb.Document copyToProtocolBuffer() {
362 DocumentPb.Document.Builder docBuilder = DocumentPb.Document.newBuilder();
363 if (documentId != null) {
364 docBuilder.setId(documentId);
366 if (locale != null) {
367 docBuilder.setLanguage(locale.toString());
369 for (Field field : fields) {
370 docBuilder.addField(field.copyToProtocolBuffer());
372 docBuilder.setOrderId(rank);
373 return DocumentChecker.checkValid(docBuilder.build());
376 @Override
377 public String toString() {
378 return String.format("Document(documentId=%s, fields=%s%s, rank=%d)",
379 documentId,
380 Util.iterableToString(fields, MAX_FIELDS_TO_STRING),
381 Util.fieldToString("locale", locale), rank);
384 boolean isIdenticalTo(Document other) {
385 if (documentId == null) {
386 if (other.documentId != null) {
387 return false;
389 } else {
390 if (!documentId.equals(other.documentId)) {
391 return false;
394 if (fields == null) {
395 if (other.fields != null) {
396 return false;
398 } else {
399 if (!fields.equals(other.fields)) {
400 return false;
403 if (locale == null) {
404 if (other.locale != null) {
405 return false;
407 } else {
408 if (!locale.equals(other.locale)) {
409 return false;
412 return rank == other.rank;