Version 1.7.4
[gae.git] / java / src / main / com / google / appengine / api / search / Field.java
blob9265bcc744b3e8774699ee5dd8ab69cb9045ec85
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.Calendar;
16 import java.util.Date;
17 import java.util.GregorianCalendar;
18 import java.util.Locale;
19 import java.util.TimeZone;
21 /**
22 * Represents a field of a {@link Document}, which is a name, an optional
23 * locale, and at most one value: text, HTML, atom, date or GeoPoint. Field
24 * name lengths are between 1 and {@link FieldChecker#MAXIMUM_NAME_LENGTH}
25 * characters, and text and HTML values are limited to
26 * {@link FieldChecker#MAXIMUM_TEXT_LENGTH}. Atoms as limited to
27 * {@link FieldChecker#MAXIMUM_ATOM_LENGTH} characters, and dates
28 * must not have a time component.
30 * <p>There are 3 types of text fields, ATOM, TEXT, and HTML. Atom fields
31 * when queried, are checked for equality. For example, if you add a field
32 * with name {@code code} and an ATOM value of "928A 33B-1", then query
33 * {@code code:"928A 33B-1"} would match the document with this field, while
34 * query {@code code:928A} would not. TEXT fields, unlike ATOM, match both
35 * on equality or if any token extracted from the original field matches.
36 * Thus if {@code code} field had the value set with
37 * {@link Field.Builder#setText(String)} method, both queries would match.
38 * Finally, HTML fields have HTML tags stripped before tokenization.
41 public final class Field implements Serializable {
43 /**
44 * A field builder. Fields must have a name, and optionally a locale
45 * and at most one of text, html, atom or date.
47 public static final class Builder {
48 private String name;
49 private Locale locale;
50 private FieldType type; private String text; private String html; private String atom; private Date date; private Double number; private GeoPoint geoPoint;
52 /**
53 * Constructs a field builder.
55 private Builder() {
58 /**
59 * Sets a name for the field. The field name length must be
60 * between 1 and {@literal FieldChecker#MAXIMUM_NAME_LENGTH} and it should match
61 * {@link FieldChecker#FIELD_NAME_PATTERN}.
63 * @param name the name of the field
64 * @return this builder
65 * @throws IllegalArgumentException if the name or value is invalid
67 public Builder setName(String name) {
68 this.name = FieldChecker.checkFieldName(name);
69 return this;
72 /**
73 * Sets a text value for the field.
75 * @param text the text value of the field
76 * @return this builder
77 * @throws IllegalArgumentException if the text is invalid
79 public Builder setText(String text) {
80 Preconditions.checkArgument(type == null, "Field value must not be already set");
81 this.type = FieldType.TEXT;
82 this.text = FieldChecker.checkText(text);
83 return this;
86 /**
87 * Sets a HTML value for the field.
89 * @param html the HTML value of the field
90 * @return this builder
91 * @throws IllegalArgumentException if the HTML is invalid
93 public Builder setHTML(String html) {
94 Preconditions.checkArgument(type == null, "Field value must not be already set");
95 this.type = FieldType.HTML;
96 this.html = FieldChecker.checkHTML(html);
97 return this;
101 * Sets an atomic value, indivisible text, for the field.
103 * @param atom the indivisible text of the field
104 * @return this builder
105 * @throws IllegalArgumentException if the atom is invalid
107 public Builder setAtom(String atom) {
108 Preconditions.checkArgument(type == null, "Field value must not be already set");
109 this.type = FieldType.ATOM;
110 this.atom = FieldChecker.checkAtom(atom);
111 return this;
115 * Sets a date associated with the field.
117 * @param date the date of the field
118 * @return this builder
119 * @throws IllegalArgumentException if the date is out of range
121 public Builder setDate(Date date) {
122 Preconditions.checkArgument(type == null, "Field value must not be already set");
123 Preconditions.checkArgument(date != null, "Cannot set date field to null.");
124 this.type = FieldType.DATE;
125 this.date = FieldChecker.checkDate(date);
126 return this;
130 * Sets a numeric value for the field. The {@code number} must be
131 * between {@link FieldChecker#MIN_NUMBER_VALUE} and
132 * {@link FieldChecker#MAX_NUMBER_VALUE}.
134 * @param number the numeric value of the field
135 * @return this builder
136 * @throws IllegalArgumentException if the number is outside the valid range
138 public Builder setNumber(double number) {
139 Preconditions.checkArgument(type == null, "Field value must not be already set");
140 this.type = FieldType.NUMBER;
141 this.number = FieldChecker.checkNumber(Double.valueOf(number));
142 return this;
146 * Sets a {@link GeoPoint} value for the field.
148 * @param geoPoint the {@link GeoPoint} value of the field
149 * @return this builder
151 public Builder setGeoPoint(GeoPoint geoPoint) {
152 Preconditions.checkArgument(type == null, "Field value must not be already set");
153 Preconditions.checkArgument(geoPoint != null, "Cannot set geo field to null.");
154 this.type = FieldType.GEO_POINT;
155 this.geoPoint = geoPoint;
156 return this;
160 * Sets the Locale of the field value. If none is given, then the locale
161 * of the document will be used.
163 * @param locale the locale the field value is written in
164 * @return this builder
166 public Builder setLocale(Locale locale) {
167 this.locale = locale;
168 return this;
172 * Builds a field using this builder. The field must have a
173 * valid name, string value, type.
175 * @return a {@link Field} built by this builder
176 * @throws IllegalArgumentException if the field has an invalid
177 * name, text, HTML, atom, date
179 public Field build() {
180 return new Field(this);
185 * The type of the field value.
187 public enum FieldType {
189 * Text content.
191 TEXT,
193 * HTML content.
195 HTML,
197 * An indivisible text content.
199 ATOM,
201 * A Date with no time component.
203 DATE,
206 * Double precision floating-point number.
208 NUMBER,
210 * A Date with no time component.
212 GEO_POINT,
215 private static final long serialVersionUID = 6829483617830682721L;
218 * Get a UTC calendar.
220 private static Calendar getCalendar() {
221 return new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.US);
224 private final String name;
225 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;
228 * Constructs a field using the builder.
230 * @param builder a builder used to construct the Field
232 private Field(Builder builder) {
233 name = builder.name;
234 type = builder.type;
235 if (builder.type != null) {
236 switch (builder.type) {
237 case TEXT:
238 text = builder.text;
239 break;
240 case HTML:
241 html = builder.html;
242 break;
243 case ATOM:
244 atom = builder.atom;
245 break;
246 case DATE:
247 date = builder.date;
248 break;
249 case NUMBER:
250 number = builder.number;
251 break;
252 case GEO_POINT:
253 geoPoint = builder.geoPoint;
254 break;
255 default:
256 throw new IllegalArgumentException(String.format("Unknown field type given %s",
257 builder.type));
260 locale = builder.locale;
261 checkValid();
265 * @return the name of the field
267 public String getName() {
268 return name;
272 * @return the type of value of the field. Can be null
274 public FieldType getType() {
275 return type;
279 * @return the text value of the field. Can be null
281 public String getText() {
282 return text;
286 * @return the HTML value of the field. Can be null
288 public String getHTML() {
289 return html;
293 * @return the atomic value of the field. Can be null
295 public String getAtom() {
296 return atom;
300 * @return the date value of the field. Can be null
302 public Date getDate() {
303 return date;
307 * @return the numeric value of the field. Can be null
309 public Double getNumber() {
310 return number;
314 * @return the {@link GeoPoint} value of the field. Can be null
316 public GeoPoint getGeoPoint() {
317 return geoPoint;
321 * @return the locale the field value is written in. Can be null. If none
322 * is given the locale of the document will be used
324 public Locale getLocale() {
325 return locale;
328 @Override
329 public int hashCode() {
330 return name.hashCode();
333 @Override
334 public boolean equals(Object object) {
335 if (object == this) {
336 return true;
338 if (!(object instanceof Field)) {
339 return false;
341 Field field = (Field) object;
342 return Util.equalObjects(name, field.name);
346 * Checks whether the field is valid, specifically,
347 * whether the field name, value are valid.
348 * Also that at most one value: text, HTML, atom or date is set.
350 * @return this Field
351 * @throws IllegalArgumentException if field name, text, HTML, atom,
352 * date are invalid
354 private Field checkValid() {
355 FieldChecker.checkFieldName(name);
356 if (type != null) {
357 switch (type) {
358 case TEXT:
359 FieldChecker.checkText(text);
360 break;
361 case HTML:
362 FieldChecker.checkHTML(html);
363 break;
364 case ATOM:
365 FieldChecker.checkAtom(atom);
366 break;
367 case DATE:
368 FieldChecker.checkDate(date);
369 break;
370 case NUMBER:
371 break;
372 case GEO_POINT:
373 break;
374 default:
375 throw new IllegalArgumentException(String.format("unknown field type %s", type));
378 return this;
382 * Creates a field builder.
384 * @return a new builder for creating fields
386 public static Builder newBuilder() {
387 return new Builder();
391 * Creates a builder of a field from the given field.
393 * @param field the field protocol buffer used to create the builder
394 * @return a field builder created from the given field
395 * @throws SearchException if the field contains invalid name, text, html,
396 * atom, date
398 static Builder newBuilder(DocumentPb.Field field) {
399 FieldValue value = field.getValue();
400 Field.Builder fieldBuilder =
401 Field.newBuilder().setName(field.getName());
402 if (value.hasLanguage()) {
403 fieldBuilder.setLocale(FieldChecker.parseLocale(value.getLanguage()));
405 switch (value.getType()) {
406 case TEXT:
407 fieldBuilder.setText(value.getStringValue());
408 break;
409 case HTML:
410 fieldBuilder.setHTML(value.getStringValue());
411 break;
412 case ATOM:
413 fieldBuilder.setAtom(value.getStringValue());
414 break;
415 case NUMBER:
416 try {
417 fieldBuilder.setNumber(
418 NumberFormat.getNumberInstance().parse(value.getStringValue()).doubleValue());
419 } catch (ParseException e) {
420 throw new SearchException("Failed to parse double: " + value.getStringValue());
422 break;
423 case GEO:
424 fieldBuilder.setGeoPoint(GeoPoint.newGeoPoint(value.getGeo()));
425 break;
426 case DATE:
427 String dateString = value.getStringValue();
428 if (dateString == null || dateString.isEmpty()) {
429 throw new SearchException(
430 String.format("date not specified for field %s", field.getName()));
432 fieldBuilder.setDate(DateUtil.deserializeDate(dateString));
433 break;
434 default:
435 throw new SearchException(
436 String.format("unknown field value type %d for field %s", value.getType(),
437 field.getName()));
439 return fieldBuilder;
443 * Copies a {@link Field} object into a {@link DocumentPb.Field} protocol
444 * buffer.
446 * @return the field protocol buffer copy of this field object
447 * @throws IllegalArgumentException if the field value type is unknown
449 DocumentPb.Field copyToProtocolBuffer() {
450 DocumentPb.FieldValue.Builder fieldValueBuilder = DocumentPb.FieldValue.newBuilder();
451 if (locale != null) {
452 fieldValueBuilder.setLanguage(locale.toString());
454 if (type != null) {
455 switch (type) {
456 case TEXT:
457 if (text != null) {
458 fieldValueBuilder.setStringValue(text);
460 fieldValueBuilder.setType(ContentType.TEXT);
461 break;
462 case HTML:
463 if (html != null) {
464 fieldValueBuilder.setStringValue(html);
466 fieldValueBuilder.setType(ContentType.HTML);
467 break;
468 case ATOM:
469 if (atom != null) {
470 fieldValueBuilder.setStringValue(atom);
472 fieldValueBuilder.setType(ContentType.ATOM);
473 break;
474 case DATE:
475 fieldValueBuilder.setStringValue(DateUtil.serializeDate(date));
476 fieldValueBuilder.setType(ContentType.DATE);
477 break;
478 case NUMBER:
479 DecimalFormat format = new DecimalFormat();
480 format.setDecimalSeparatorAlwaysShown(false);
481 format.setGroupingUsed(false);
482 fieldValueBuilder.setStringValue(format.format(number));
483 fieldValueBuilder.setType(ContentType.NUMBER);
484 break;
485 case GEO_POINT:
486 fieldValueBuilder.setGeo(geoPoint.copyToProtocolBuffer());
487 fieldValueBuilder.setType(ContentType.GEO);
488 break;
489 default:
490 throw new IllegalArgumentException(String.format("unknown field type %s", type));
494 DocumentPb.Field.Builder builder = DocumentPb.Field.newBuilder()
495 .setName(name)
496 .setValue(fieldValueBuilder);
497 return builder.build();
500 @Override
501 public String toString() {
502 return String.format("Field(name=%s%s, type=%s%s)",
503 name,
504 Util.fieldToString("value", valueToString()),
505 type.toString(),
506 Util.fieldToString("locale", locale));
509 private String valueToString() throws IllegalArgumentException {
510 switch (type) {
511 case TEXT:
512 return text;
513 case HTML:
514 return html;
515 case ATOM:
516 return atom;
517 case DATE:
518 return DateUtil.formatDateTime(date);
519 case GEO_POINT:
520 return geoPoint.toString();
521 case NUMBER:
522 DecimalFormat format = new DecimalFormat();
523 format.setDecimalSeparatorAlwaysShown(false);
524 return format.format(number);
525 default:
526 throw new IllegalArgumentException(String.format("unknown field type %s", type));
531 * Returns a date which has been truncated to a day of month. Deprecated.
533 * @param date the date to be truncated
534 * @return the date with fields less significant than
535 * {@link Calendar#DAY_OF_MONTH}
536 * @deprecated as of 1.7.2 this is no longer required for Date fields
538 @Deprecated
539 public static Date date(Date date) {
540 return truncate(date, Calendar.DAY_OF_MONTH);
544 * Truncates given date leaving date elements lesser than the
545 * specified field set to 0. For example, if you wish to remove
546 * time component from the date use the following:
547 * <pre>
548 * Date d = ...
549 * Date yearMonthDay = Field.truncate(d, Calendar.DAY_OF_MONTH);
550 * </pre>
552 * @param date the date to be truncated
553 * @param field the least significant field to be left untouched
554 * @return the date with fields less significant than field set to 0
555 * @throws IllegalArgumentException if field is not a valid datetime field.
556 * @deprecated as of 1.7.2 this is no longer required for Date fields
558 @Deprecated
559 public static Date truncate(Date date, int field) {
560 return DateUtil.truncate(date, field);