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
.text
.NumberFormat
;
14 import java
.text
.ParseException
;
15 import java
.util
.Date
;
16 import java
.util
.Locale
;
19 * Represents a field of a {@link Document}, which is a name, an optional locale, and at most one
20 * value: text, HTML, atom, date or GeoPoint. Field name lengths are between 1 and
21 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_NAME_LENGTH} characters,
22 * and text and HTML values are limited to
23 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_TEXT_LENGTH}. Atoms as
24 * limited to {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_ATOM_LENGTH}
25 * characters, and dates must not have a time component.
28 * There are 3 types of text fields, ATOM, TEXT, and HTML. Atom fields when queried, are checked for
29 * equality. For example, if you add a field with name {@code code} and an ATOM value of "928A
30 * 33B-1", then query {@code code:"928A 33B-1"} would match the document with this field, while
31 * query {@code code:928A} would not. TEXT fields, unlike ATOM, match both on equality or if any
32 * token extracted from the original field matches. Thus if {@code code} field had the value set
33 * with {@link Field.Builder#setText(String)} method, both queries would match. Finally, HTML fields
34 * have HTML tags stripped before tokenization.
36 public final class Field
implements Serializable
{
39 * A field builder. Fields must have a name, and optionally a locale
40 * and at most one of text, html, atom or date.
42 public static final class Builder
{
44 private Locale locale
;
45 private FieldType type
; private String text
; private String html
; private String atom
; private Date date
; private Double number
; private GeoPoint geoPoint
;
48 * Constructs a field builder.
54 * Sets a name for the field. The field name length must be
55 * between 1 and {@literal FieldChecker#MAXIMUM_NAME_LENGTH} and it should match
56 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#FIELD_NAME_PATTERN}.
58 * @param name the name of the field
59 * @return this builder
60 * @throws IllegalArgumentException if the name or value is invalid
62 public Builder
setName(String name
) {
63 this.name
= FieldChecker
.checkFieldName(name
);
68 * Sets a text value for the field.
70 * @param text the text value of the field
71 * @return this builder
72 * @throws IllegalArgumentException if the text is invalid
74 public Builder
setText(String text
) {
75 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
76 this.type
= FieldType
.TEXT
;
77 this.text
= FieldChecker
.checkText(text
);
82 * Sets a HTML value for the field.
84 * @param html the HTML value of the field
85 * @return this builder
86 * @throws IllegalArgumentException if the HTML is invalid
88 public Builder
setHTML(String html
) {
89 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
90 this.type
= FieldType
.HTML
;
91 this.html
= FieldChecker
.checkHTML(html
);
96 * Sets an atomic value, indivisible text, for the field.
98 * @param atom the indivisible text of the field
99 * @return this builder
100 * @throws IllegalArgumentException if the atom is invalid
102 public Builder
setAtom(String atom
) {
103 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
104 this.type
= FieldType
.ATOM
;
105 this.atom
= FieldChecker
.checkAtom(atom
);
110 * Sets a date associated with the field.
112 * @param date the date of the field
113 * @return this builder
114 * @throws IllegalArgumentException if the date is out of range
116 public Builder
setDate(Date date
) {
117 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
118 Preconditions
.checkArgument(date
!= null, "Cannot set date field to null.");
119 this.type
= FieldType
.DATE
;
120 this.date
= FieldChecker
.checkDate(date
);
125 * Sets a numeric value for the field. The {@code number} must be between
126 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MINIMUM_NUMBER_VALUE} and
127 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_NUMBER_VALUE}.
129 * @param number the numeric value of the field
130 * @return this builder
131 * @throws IllegalArgumentException if the number is outside the valid range
133 public Builder
setNumber(double number
) {
134 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
135 this.type
= FieldType
.NUMBER
;
136 this.number
= FieldChecker
.checkNumber(Double
.valueOf(number
));
141 * Sets a {@link GeoPoint} value for the field.
143 * @param geoPoint the {@link GeoPoint} value of the field
144 * @return this builder
146 public Builder
setGeoPoint(GeoPoint geoPoint
) {
147 Preconditions
.checkArgument(type
== null, "Field value must not be already set");
148 Preconditions
.checkArgument(geoPoint
!= null, "Cannot set geo field to null.");
149 this.type
= FieldType
.GEO_POINT
;
150 this.geoPoint
= geoPoint
;
155 * Sets the Locale of the field value. If none is given, then the locale
156 * of the document will be used.
158 * @param locale the locale the field value is written in
159 * @return this builder
161 public Builder
setLocale(Locale locale
) {
162 this.locale
= locale
;
167 * Builds a field using this builder. The field must have a
168 * valid name, string value, type.
170 * @return a {@link Field} built by this builder
171 * @throws IllegalArgumentException if the field has an invalid
172 * name, text, HTML, atom, date
174 public Field
build() {
175 return new Field(this);
180 * The type of the field value.
182 public enum FieldType
{
192 * An indivisible text content.
196 * A Date with no time component.
201 * Double precision floating-point number.
205 * Geographical coordinates of a point, in WGS84.
210 private static final long serialVersionUID
= 6829483617830682721L;
212 private final String name
;
213 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
;
216 * Constructs a field using the builder.
218 * @param builder a builder used to construct the Field
220 private Field(Builder builder
) {
223 if (builder
.type
!= null) {
224 switch (builder
.type
) {
238 number
= builder
.number
;
241 geoPoint
= builder
.geoPoint
;
244 throw new IllegalArgumentException(String
.format("Unknown field type given %s",
248 locale
= builder
.locale
;
253 * @return the name of the field
255 public String
getName() {
260 * @return the type of value of the field. Can be null
262 public FieldType
getType() {
267 * @return the text value of the field. Can be null
269 public String
getText() {
274 * @return the HTML value of the field. Can be null
276 public String
getHTML() {
281 * @return the atomic value of the field. Can be null
283 public String
getAtom() {
288 * @return the date value of the field. Can be null
290 public Date
getDate() {
295 * @return the numeric value of the field. Can be null
297 public Double
getNumber() {
302 * @return the {@link GeoPoint} value of the field. Can be null
304 public GeoPoint
getGeoPoint() {
309 * @return the locale the field value is written in. Can be null. If none
310 * is given the locale of the document will be used
312 public Locale
getLocale() {
317 public int hashCode() {
318 return name
.hashCode();
322 public boolean equals(Object object
) {
323 if (object
== this) {
326 if (!(object
instanceof Field
)) {
329 Field field
= (Field
) object
;
330 return Util
.equalObjects(name
, field
.name
);
334 * Checks whether the field is valid, specifically,
335 * whether the field name, value are valid.
336 * Also that at most one value: text, HTML, atom or date is set.
339 * @throws IllegalArgumentException if field name, text, HTML, atom,
342 private Field
checkValid() {
343 FieldChecker
.checkFieldName(name
);
347 FieldChecker
.checkText(text
);
350 FieldChecker
.checkHTML(html
);
353 FieldChecker
.checkAtom(atom
);
356 FieldChecker
.checkDate(date
);
363 throw new IllegalArgumentException(String
.format("unknown field type %s", type
));
370 * Creates a field builder.
372 * @return a new builder for creating fields
374 public static Builder
newBuilder() {
375 return new Builder();
379 * Creates a builder of a field from the given field.
381 * @param field the field protocol buffer used to create the builder
382 * @return a field builder created from the given field
383 * @throws SearchException if the field contains invalid name, text, html,
386 static Builder
newBuilder(DocumentPb
.Field field
) {
387 FieldValue value
= field
.getValue();
388 Field
.Builder fieldBuilder
=
389 Field
.newBuilder().setName(field
.getName());
390 if (value
.hasLanguage()) {
391 fieldBuilder
.setLocale(FieldChecker
.parseLocale(value
.getLanguage()));
393 switch (value
.getType()) {
395 fieldBuilder
.setText(value
.getStringValue());
398 fieldBuilder
.setHTML(value
.getStringValue());
401 fieldBuilder
.setAtom(value
.getStringValue());
405 fieldBuilder
.setNumber(
406 NumberFormat
.getNumberInstance().parse(value
.getStringValue()).doubleValue());
407 } catch (ParseException e
) {
408 throw new SearchException("Failed to parse double: " + value
.getStringValue());
412 fieldBuilder
.setGeoPoint(GeoPoint
.newGeoPoint(value
.getGeo()));
415 String dateString
= value
.getStringValue();
416 if (dateString
== null || dateString
.isEmpty()) {
417 throw new SearchException(
418 String
.format("date not specified for field %s", field
.getName()));
420 fieldBuilder
.setDate(DateUtil
.deserializeDate(dateString
));
423 throw new SearchException(
424 String
.format("unknown field value type %s for field %s", value
.getType(),
431 * Copies a {@link Field} object into a {@link com.google.apphosting.api.search.DocumentPb.Field}
434 * @return the field protocol buffer copy of this field object
435 * @throws IllegalArgumentException if the field value type is unknown
437 DocumentPb
.Field
copyToProtocolBuffer() {
438 DocumentPb
.FieldValue
.Builder fieldValueBuilder
= DocumentPb
.FieldValue
.newBuilder();
439 if (locale
!= null) {
440 fieldValueBuilder
.setLanguage(locale
.toString());
446 fieldValueBuilder
.setStringValue(text
);
448 fieldValueBuilder
.setType(ContentType
.TEXT
);
452 fieldValueBuilder
.setStringValue(html
);
454 fieldValueBuilder
.setType(ContentType
.HTML
);
458 fieldValueBuilder
.setStringValue(atom
);
460 fieldValueBuilder
.setType(ContentType
.ATOM
);
463 fieldValueBuilder
.setStringValue(DateUtil
.serializeDate(date
));
464 fieldValueBuilder
.setType(ContentType
.DATE
);
467 DecimalFormat format
= new DecimalFormat();
468 format
.setDecimalSeparatorAlwaysShown(false);
469 format
.setGroupingUsed(false);
470 format
.setMaximumFractionDigits(Integer
.MAX_VALUE
);
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 new Util
.ToStringHelper("Field")
492 .addField("name", name
)
493 .addField("value", valueToString())
494 .addField("type", type
.toString())
495 .addField("locale", locale
)
499 private String
valueToString() throws IllegalArgumentException
{
508 return DateUtil
.formatDateTime(date
);
510 return geoPoint
.toString();
512 DecimalFormat format
= new DecimalFormat();
513 format
.setDecimalSeparatorAlwaysShown(false);
514 format
.setMaximumFractionDigits(Integer
.MAX_VALUE
);
515 return format
.format(number
);
517 throw new IllegalArgumentException(String
.format("unknown field type %s", type
));