1 // Copyright 2010 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.api
.search
;
5 import com
.google
.appengine
.api
.search
.checkers
.FieldChecker
;
6 import com
.google
.appengine
.api
.search
.checkers
.Preconditions
;
7 import com
.google
.apphosting
.api
.search
.DocumentPb
;
8 import com
.google
.apphosting
.api
.search
.DocumentPb
.FieldValue
;
9 import com
.google
.apphosting
.api
.search
.DocumentPb
.FieldValue
.ContentType
;
11 import java
.io
.Serializable
;
12 import java
.text
.DecimalFormat
;
13 import java
.util
.Calendar
;
14 import java
.util
.Date
;
15 import java
.util
.GregorianCalendar
;
16 import java
.util
.Locale
;
17 import java
.util
.TimeZone
;
20 * Represents a field of a {@link Document}, which is a name, an optional
21 * locale, and at most one value: text, HTML, atom, date or GeoPoint. Field
22 * name lengths are between 1 and {@link FieldChecker#MAXIMUM_NAME_LENGTH}
23 * characters, and text and HTML values are limited to
24 * {@link FieldChecker#MAXIMUM_TEXT_LENGTH}. Atoms as limited to
25 * {@link FieldChecker#MAXIMUM_ATOM_LENGTH} characters, and dates
26 * must not have a time component.
28 * <p>There are 3 types of text fields, ATOM, TEXT, and HTML. Atom fields
29 * when queried, are checked for equality. For example, if you add a field
30 * with name {@code code} and an ATOM value of "928A 33B-1", then query
31 * {@code code:"928A 33B-1"} would match the document with this field, while
32 * query {@code code:928A} would not. TEXT fields, unlike ATOM, match both
33 * on equality or if any token extracted from the original field matches.
34 * Thus if {@code code} field had the value set with
35 * {@link Field.Builder#setText(String)} method, both queries would match.
36 * Finally, HTML fields have HTML tags stripped before tokenization.
39 public final class Field
implements Serializable
{
42 * A field builder. Fields must have a name, and optionally a locale
43 * and at most one of text, html, atom or date.
45 public static final class Builder
{
47 private Locale locale
;
48 private FieldType type
; private String text
; private String html
; private String atom
; private Date date
; private Double number
; private GeoPoint geoPoint
;
51 * Constructs a field builder.
57 * Sets a name for the field. The field name length must be
58 * between 1 and {@literal FieldChecker#MAXIMUM_NAME_LENGTH} and it should match
59 * {@link FieldChecker#FIELD_NAME_PATTERN}.
61 * @param name the name of the field
62 * @return this builder
63 * @throws IllegalArgumentException if the name or value is invalid
65 public Builder
setName(String name
) {
66 this.name
= FieldChecker
.checkFieldName(name
);
71 * Sets a text value for the field.
73 * @param text the text value of the field
74 * @return this builder
75 * @throws IllegalArgumentException if the text is invalid
77 public Builder
setText(String text
) {
78 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
79 this.type
= FieldType
.TEXT
;
80 this.text
= FieldChecker
.checkText(text
);
85 * Sets a HTML value for the field.
87 * @param html the HTML value of the field
88 * @return this builder
89 * @throws IllegalArgumentException if the HTML is invalid
91 public Builder
setHTML(String html
) {
92 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
93 this.type
= FieldType
.HTML
;
94 this.html
= FieldChecker
.checkHTML(html
);
99 * Sets an atomic value, indivisible text, for the field.
101 * @param atom the indivisible text of the field
102 * @return this builder
103 * @throws IllegalArgumentException if the atom is invalid
105 public Builder
setAtom(String atom
) {
106 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
107 this.type
= FieldType
.ATOM
;
108 this.atom
= FieldChecker
.checkAtom(atom
);
113 * Sets a date associated with the field.
115 * @param date the date of the field
116 * @return this builder
117 * @throws IllegalArgumentException if the date is out of range
119 public Builder
setDate(Date date
) {
120 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
121 this.type
= FieldType
.DATE
;
122 this.date
= FieldChecker
.checkDate(date
);
127 * Sets a numeric value for the field. The {@code number} must be
128 * between {@link FieldChecker#MIN_NUMBER_VALUE} and
129 * {@link FieldChecker#MAX_NUMBER_VALUE}.
131 * @param number the numeric value of the field
132 * @return this builder
133 * @throws IllegalArgumentException if the number is outside the valid range
135 public Builder
setNumber(double number
) {
136 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
137 this.type
= FieldType
.NUMBER
;
138 this.number
= FieldChecker
.checkNumber(Double
.valueOf(number
));
143 * Sets a {@link GeoPoint} value for the field.
145 * @param geoPoint the {@link GeoPoint} value of the field
146 * @return this builder
148 public Builder
setGeoPoint(GeoPoint geoPoint
) {
149 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
150 this.type
= FieldType
.GEO_POINT
;
151 this.geoPoint
= geoPoint
;
156 * Sets the Locale of the field value. If none is given, then the locale
157 * of the document will be used.
159 * @param locale the locale the field value is written in
160 * @return this builder
162 public Builder
setLocale(Locale locale
) {
163 this.locale
= locale
;
168 * Builds a field using this builder. The field must have a
169 * valid name, string value, type.
171 * @return a {@link Field} built by this builder
172 * @throws IllegalArgumentException if the field has an invalid
173 * name, text, HTML, atom, date
175 public Field
build() {
176 return new Field(this);
181 * The type of the field value.
183 public enum FieldType
{
193 * An indivisible text content.
197 * A Date with no time component.
202 * Double precision floating-point number.
206 * A Date with no time component.
211 private static final long serialVersionUID
= 6829483617830682721L;
214 * Get a UTC calendar.
216 private static Calendar
getCalendar() {
217 return new GregorianCalendar(TimeZone
.getTimeZone("UTC"), Locale
.US
);
220 private final String name
;
221 private final Locale locale
; private final FieldType type
; private String text
; private String html
; private String atom
; private Date date
; private Double number
; private GeoPoint geoPoint
;
224 * Constructs a field using the builder.
226 * @param builder a builder used to construct the Field
228 private Field(Builder builder
) {
231 if (builder
.type
!= null) {
232 switch (builder
.type
) {
246 number
= builder
.number
;
249 geoPoint
= builder
.geoPoint
;
252 throw new IllegalArgumentException(String
.format("Unknown field type given %s",
256 locale
= builder
.locale
;
261 * @return the name of the field
263 public String
getName() {
268 * @return the type of value of the field. Can be null
270 public FieldType
getType() {
275 * @return the text value of the field. Can be null
277 public String
getText() {
282 * @return the HTML value of the field. Can be null
284 public String
getHTML() {
289 * @return the atomic value of the field. Can be null
291 public String
getAtom() {
296 * @return the date value of the field. Can be null
298 public Date
getDate() {
303 * @return the numeric value of the field. Can be null
305 public Double
getNumber() {
310 * @return the {@link GeoPoint} value of the field. Can be null
312 public GeoPoint
getGeoPoint() {
317 * @return the locale the field value is written in. Can be null. If none
318 * is given the locale of the document will be used
320 public Locale
getLocale() {
325 public int hashCode() {
326 return name
.hashCode();
330 public boolean equals(Object object
) {
331 if (object
== this) {
334 if (!(object
instanceof Field
)) {
337 Field field
= (Field
) object
;
338 return Util
.equalObjects(name
, field
.name
);
342 * Checks whether the field is valid, specifically,
343 * whether the field name, value are valid.
344 * Also that at most one value: text, HTML, atom or date is set.
347 * @throws IllegalArgumentException if field name, text, HTML, atom,
350 private Field
checkValid() {
351 FieldChecker
.checkFieldName(name
);
355 FieldChecker
.checkText(text
);
358 FieldChecker
.checkHTML(html
);
361 FieldChecker
.checkAtom(atom
);
364 FieldChecker
.checkDate(date
);
371 throw new IllegalArgumentException(String
.format("unknown field type %s", type
));
378 * Creates a field builder.
380 * @return a new builder for creating fields
382 public static Builder
newBuilder() {
383 return new Builder();
387 * Creates a builder of a field from the given field.
389 * @param field the field protocol buffer used to create the builder
390 * @return a field builder created from the given field
391 * @throws SearchException if the field contains invalid name, text, html,
394 static Builder
newBuilder(DocumentPb
.Field field
) {
395 FieldValue value
= field
.getValue();
396 Field
.Builder fieldBuilder
=
397 Field
.newBuilder().setName(field
.getName());
398 if (value
.hasLanguage()) {
399 fieldBuilder
.setLocale(FieldChecker
.parseLocale(value
.getLanguage()));
401 switch (value
.getType()) {
403 fieldBuilder
.setText(value
.getStringValue());
406 fieldBuilder
.setHTML(value
.getStringValue());
409 fieldBuilder
.setAtom(value
.getStringValue());
413 fieldBuilder
.setNumber(Double
.parseDouble(value
.getStringValue()));
414 } catch (NumberFormatException e
) {
415 throw new SearchException("Failed to parse double: " + value
.getStringValue());
419 fieldBuilder
.setGeoPoint(GeoPoint
.newGeoPoint(value
.getGeo()));
422 String dateString
= value
.getStringValue();
423 if (dateString
== null || dateString
.isEmpty()) {
424 throw new SearchException(
425 String
.format("date not specified for field %s", field
.getName()));
427 fieldBuilder
.setDate(DateUtil
.deserializeDate(dateString
));
430 throw new SearchException(
431 String
.format("unknown field value type %d for field %s", value
.getType(),
438 * Copies a {@link Field} object into a {@link DocumentPb.Field} protocol
441 * @return the field protocol buffer copy of this field object
442 * @throws IllegalArgumentException if the field value type is unknown
444 DocumentPb
.Field
copyToProtocolBuffer() {
445 DocumentPb
.FieldValue
.Builder fieldValueBuilder
= DocumentPb
.FieldValue
.newBuilder();
446 if (locale
!= null) {
447 fieldValueBuilder
.setLanguage(locale
.toString());
452 fieldValueBuilder
.setStringValue(text
);
453 fieldValueBuilder
.setType(ContentType
.TEXT
);
456 fieldValueBuilder
.setStringValue(html
);
457 fieldValueBuilder
.setType(ContentType
.HTML
);
460 fieldValueBuilder
.setStringValue(atom
);
461 fieldValueBuilder
.setType(ContentType
.ATOM
);
464 fieldValueBuilder
.setStringValue(DateUtil
.serializeDate(date
));
465 fieldValueBuilder
.setType(ContentType
.DATE
);
468 DecimalFormat format
= new DecimalFormat();
469 format
.setDecimalSeparatorAlwaysShown(false);
470 format
.setGroupingUsed(false);
471 fieldValueBuilder
.setStringValue(format
.format(number
));
472 fieldValueBuilder
.setType(ContentType
.NUMBER
);
475 fieldValueBuilder
.setGeo(geoPoint
.copyToProtocolBuffer());
476 fieldValueBuilder
.setType(ContentType
.GEO
);
479 throw new IllegalArgumentException(String
.format("unknown field type %s", type
));
483 DocumentPb
.Field
.Builder builder
= DocumentPb
.Field
.newBuilder()
485 .setValue(fieldValueBuilder
);
486 return builder
.build();
490 public String
toString() {
491 return String
.format("Field(name=%s%s, type=%s%s)",
493 Util
.fieldToString("value", valueToString()),
495 Util
.fieldToString("locale", locale
));
498 private String
valueToString() throws IllegalArgumentException
{
507 return DateUtil
.formatDateTime(date
);
509 return geoPoint
.toString();
511 DecimalFormat format
= new DecimalFormat();
512 format
.setDecimalSeparatorAlwaysShown(false);
513 return format
.format(number
);
515 throw new IllegalArgumentException(String
.format("unknown field type %s", type
));
520 * Returns a date which has been truncated to a day of month. Deprecated.
522 * @param date the date to be truncated
523 * @return the date with fields less significant than
524 * {@link Calendar#DAY_OF_MONTH}
525 * @deprecated as of 1.7.2 this is no longer required for Date fields
528 public static Date
date(Date date
) {
529 return truncate(date
, Calendar
.DAY_OF_MONTH
);
533 * Truncates given date leaving date elements lesser than the
534 * specified field set to 0. For example, if you wish to remove
535 * time component from the date use the following:
538 * Date yearMonthDay = Field.truncate(d, Calendar.DAY_OF_MONTH);
541 * @param date the date to be truncated
542 * @param field the least significant field to be left untouched
543 * @return the date with fields less significant than field set to 0
544 * @throws IllegalArgumentException if field is not a valid datetime field.
545 * @deprecated as of 1.7.2 this is no longer required for Date fields
548 public static Date
truncate(Date date
, int field
) {
549 return DateUtil
.truncate(date
, field
);