Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Field.java
blob3970b10d061be64ea3c4331aeea1158ac43d5be3
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;
18 /**
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.
27 * <p>
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 {
38 /**
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 {
43 private String name;
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;
47 /**
48 * Constructs a field builder.
50 private Builder() {
53 /**
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);
64 return this;
67 /**
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);
78 return this;
81 /**
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);
92 return this;
95 /**
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);
106 return this;
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);
121 return this;
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));
137 return this;
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;
151 return this;
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;
163 return this;
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 {
184 * Text content.
186 TEXT,
188 * HTML content.
190 HTML,
192 * An indivisible text content.
194 ATOM,
196 * A Date with no time component.
198 DATE,
201 * Double precision floating-point number.
203 NUMBER,
205 * Geographical coordinates of a point, in WGS84.
207 GEO_POINT,
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) {
221 name = builder.name;
222 type = builder.type;
223 if (builder.type != null) {
224 switch (builder.type) {
225 case TEXT:
226 text = builder.text;
227 break;
228 case HTML:
229 html = builder.html;
230 break;
231 case ATOM:
232 atom = builder.atom;
233 break;
234 case DATE:
235 date = builder.date;
236 break;
237 case NUMBER:
238 number = builder.number;
239 break;
240 case GEO_POINT:
241 geoPoint = builder.geoPoint;
242 break;
243 default:
244 throw new IllegalArgumentException(String.format("Unknown field type given %s",
245 builder.type));
248 locale = builder.locale;
249 checkValid();
253 * @return the name of the field
255 public String getName() {
256 return name;
260 * @return the type of value of the field. Can be null
262 public FieldType getType() {
263 return type;
267 * @return the text value of the field. Can be null
269 public String getText() {
270 return text;
274 * @return the HTML value of the field. Can be null
276 public String getHTML() {
277 return html;
281 * @return the atomic value of the field. Can be null
283 public String getAtom() {
284 return atom;
288 * @return the date value of the field. Can be null
290 public Date getDate() {
291 return date;
295 * @return the numeric value of the field. Can be null
297 public Double getNumber() {
298 return number;
302 * @return the {@link GeoPoint} value of the field. Can be null
304 public GeoPoint getGeoPoint() {
305 return geoPoint;
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() {
313 return locale;
316 @Override
317 public int hashCode() {
318 return name.hashCode();
321 @Override
322 public boolean equals(Object object) {
323 if (object == this) {
324 return true;
326 if (!(object instanceof Field)) {
327 return false;
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.
338 * @return this Field
339 * @throws IllegalArgumentException if field name, text, HTML, atom,
340 * date are invalid
342 private Field checkValid() {
343 FieldChecker.checkFieldName(name);
344 if (type != null) {
345 switch (type) {
346 case TEXT:
347 FieldChecker.checkText(text);
348 break;
349 case HTML:
350 FieldChecker.checkHTML(html);
351 break;
352 case ATOM:
353 FieldChecker.checkAtom(atom);
354 break;
355 case DATE:
356 FieldChecker.checkDate(date);
357 break;
358 case NUMBER:
359 break;
360 case GEO_POINT:
361 break;
362 default:
363 throw new IllegalArgumentException(String.format("unknown field type %s", type));
366 return this;
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,
384 * atom, date
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()) {
394 case TEXT:
395 fieldBuilder.setText(value.getStringValue());
396 break;
397 case HTML:
398 fieldBuilder.setHTML(value.getStringValue());
399 break;
400 case ATOM:
401 fieldBuilder.setAtom(value.getStringValue());
402 break;
403 case NUMBER:
404 try {
405 fieldBuilder.setNumber(
406 NumberFormat.getNumberInstance().parse(value.getStringValue()).doubleValue());
407 } catch (ParseException e) {
408 throw new SearchException("Failed to parse double: " + value.getStringValue());
410 break;
411 case GEO:
412 fieldBuilder.setGeoPoint(GeoPoint.newGeoPoint(value.getGeo()));
413 break;
414 case DATE:
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));
421 break;
422 default:
423 throw new SearchException(
424 String.format("unknown field value type %d for field %s", value.getType(),
425 field.getName()));
427 return fieldBuilder;
431 * Copies a {@link Field} object into a {@link com.google.apphosting.api.search.DocumentPb.Field}
432 * protocol buffer.
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());
442 if (type != null) {
443 switch (type) {
444 case TEXT:
445 if (text != null) {
446 fieldValueBuilder.setStringValue(text);
448 fieldValueBuilder.setType(ContentType.TEXT);
449 break;
450 case HTML:
451 if (html != null) {
452 fieldValueBuilder.setStringValue(html);
454 fieldValueBuilder.setType(ContentType.HTML);
455 break;
456 case ATOM:
457 if (atom != null) {
458 fieldValueBuilder.setStringValue(atom);
460 fieldValueBuilder.setType(ContentType.ATOM);
461 break;
462 case DATE:
463 fieldValueBuilder.setStringValue(DateUtil.serializeDate(date));
464 fieldValueBuilder.setType(ContentType.DATE);
465 break;
466 case NUMBER:
467 DecimalFormat format = new DecimalFormat();
468 format.setDecimalSeparatorAlwaysShown(false);
469 format.setGroupingUsed(false);
470 fieldValueBuilder.setStringValue(format.format(number));
471 fieldValueBuilder.setType(ContentType.NUMBER);
472 break;
473 case GEO_POINT:
474 fieldValueBuilder.setGeo(geoPoint.copyToProtocolBuffer());
475 fieldValueBuilder.setType(ContentType.GEO);
476 break;
477 default:
478 throw new IllegalArgumentException(String.format("unknown field type %s", type));
482 DocumentPb.Field.Builder builder = DocumentPb.Field.newBuilder()
483 .setName(name)
484 .setValue(fieldValueBuilder);
485 return builder.build();
488 @Override
489 public String toString() {
490 return new Util.ToStringHelper("Field")
491 .addField("name", name)
492 .addField("value", valueToString())
493 .addField("type", type.toString())
494 .addField("locale", locale)
495 .finish();
498 private String valueToString() throws IllegalArgumentException {
499 switch (type) {
500 case TEXT:
501 return text;
502 case HTML:
503 return html;
504 case ATOM:
505 return atom;
506 case DATE:
507 return DateUtil.formatDateTime(date);
508 case GEO_POINT:
509 return geoPoint.toString();
510 case NUMBER:
511 DecimalFormat format = new DecimalFormat();
512 format.setDecimalSeparatorAlwaysShown(false);
513 return format.format(number);
514 default:
515 throw new IllegalArgumentException(String.format("unknown field type %s", type));