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
;
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
25 * Document document = Document.newBuilder().setId("document id")
26 * .setLocale(Locale.UK)
27 * .addField(Field.newBuilder()
29 * .setText("going for dinner"))
30 * .addField(Field.newBuilder()
32 * .setHTML("<html>I found a restaurant.</html>"))
33 * .addField(Field.newBuilder()
34 * .setName("signature")
35 * .setText("ten post jest przeznaczony dla odbiorcy")
36 * .setLocale(new Locale("pl")))
37 * .addField(Field.newBuilder()
40 * .addField(Field.newBuilder()
46 * The following example shows how to access the fields within a document:
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;
60 public class Document
implements Serializable
{
62 static final int MAX_FIELDS_TO_STRING
= 10;
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
;
75 * Constructs a builder for a document.
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
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
);
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());
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
);
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
;
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
) {
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());
198 * @return an iterable of {@link Field} in the document
200 public Iterable
<Field
> getFields() {
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
218 public Iterable
<Field
> getFields(String name
) {
219 List
<Field
> fieldsForName
= fieldMap
.get(name
);
220 if (fieldsForName
== 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
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() {
263 * @return the {@link Locale} the document is written in. Can be null
265 public Locale
getLocale() {
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() {
281 public int hashCode() {
282 return documentId
.hashCode();
286 public boolean equals(Object object
) {
287 if (this == object
) {
290 if (!(object
instanceof Document
)) {
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
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");
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
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());
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());
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
)
385 boolean isIdenticalTo(Document other
) {
386 if (documentId
== null) {
387 if (other
.documentId
!= null) {
391 if (!documentId
.equals(other
.documentId
)) {
395 if (fields
== null) {
396 if (other
.fields
!= null) {
400 if (!fields
.equals(other
.fields
)) {
404 if (locale
== null) {
405 if (other
.locale
!= null) {
409 if (!locale
.equals(other
.locale
)) {
413 return rank
== other
.rank
;