Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Field.java
blob9a6205fa8c61b9c93cfa3a7581c477fb8fbde3f0
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;
19 /**
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 {
41 /**
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 {
46 private String name;
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;
50 /**
51 * Constructs a field builder.
53 private Builder() {
56 /**
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);
67 return this;
70 /**
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);
81 return this;
84 /**
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);
95 return this;
98 /**
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);
109 return this;
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);
123 return this;
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));
139 return this;
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;
152 return this;
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;
164 return this;
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 {
185 * Text content.
187 TEXT,
189 * HTML content.
191 HTML,
193 * An indivisible text content.
195 ATOM,
197 * A Date with no time component.
199 DATE,
202 * Double precision floating-point number.
204 NUMBER,
206 * A Date with no time component.
208 GEO_POINT,
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) {
229 name = builder.name;
230 type = builder.type;
231 if (builder.type != null) {
232 switch (builder.type) {
233 case TEXT:
234 text = builder.text;
235 break;
236 case HTML:
237 html = builder.html;
238 break;
239 case ATOM:
240 atom = builder.atom;
241 break;
242 case DATE:
243 date = builder.date;
244 break;
245 case NUMBER:
246 number = builder.number;
247 break;
248 case GEO_POINT:
249 geoPoint = builder.geoPoint;
250 break;
251 default:
252 throw new IllegalArgumentException(String.format("Unknown field type given %s",
253 builder.type));
256 locale = builder.locale;
257 checkValid();
261 * @return the name of the field
263 public String getName() {
264 return name;
268 * @return the type of value of the field. Can be null
270 public FieldType getType() {
271 return type;
275 * @return the text value of the field. Can be null
277 public String getText() {
278 return text;
282 * @return the HTML value of the field. Can be null
284 public String getHTML() {
285 return html;
289 * @return the atomic value of the field. Can be null
291 public String getAtom() {
292 return atom;
296 * @return the date value of the field. Can be null
298 public Date getDate() {
299 return date;
303 * @return the numeric value of the field. Can be null
305 public Double getNumber() {
306 return number;
310 * @return the {@link GeoPoint} value of the field. Can be null
312 public GeoPoint getGeoPoint() {
313 return geoPoint;
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() {
321 return locale;
324 @Override
325 public int hashCode() {
326 return name.hashCode();
329 @Override
330 public boolean equals(Object object) {
331 if (object == this) {
332 return true;
334 if (!(object instanceof Field)) {
335 return false;
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.
346 * @return this Field
347 * @throws IllegalArgumentException if field name, text, HTML, atom,
348 * date are invalid
350 private Field checkValid() {
351 FieldChecker.checkFieldName(name);
352 if (type != null) {
353 switch (type) {
354 case TEXT:
355 FieldChecker.checkText(text);
356 break;
357 case HTML:
358 FieldChecker.checkHTML(html);
359 break;
360 case ATOM:
361 FieldChecker.checkAtom(atom);
362 break;
363 case DATE:
364 FieldChecker.checkDate(date);
365 break;
366 case NUMBER:
367 break;
368 case GEO_POINT:
369 break;
370 default:
371 throw new IllegalArgumentException(String.format("unknown field type %s", type));
374 return this;
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,
392 * atom, date
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()) {
402 case TEXT:
403 fieldBuilder.setText(value.getStringValue());
404 break;
405 case HTML:
406 fieldBuilder.setHTML(value.getStringValue());
407 break;
408 case ATOM:
409 fieldBuilder.setAtom(value.getStringValue());
410 break;
411 case NUMBER:
412 try {
413 fieldBuilder.setNumber(Double.parseDouble(value.getStringValue()));
414 } catch (NumberFormatException e) {
415 throw new SearchException("Failed to parse double: " + value.getStringValue());
417 break;
418 case GEO:
419 fieldBuilder.setGeoPoint(GeoPoint.newGeoPoint(value.getGeo()));
420 break;
421 case DATE:
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));
428 break;
429 default:
430 throw new SearchException(
431 String.format("unknown field value type %d for field %s", value.getType(),
432 field.getName()));
434 return fieldBuilder;
438 * Copies a {@link Field} object into a {@link DocumentPb.Field} protocol
439 * buffer.
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());
449 if (type != null) {
450 switch (type) {
451 case TEXT:
452 fieldValueBuilder.setStringValue(text);
453 fieldValueBuilder.setType(ContentType.TEXT);
454 break;
455 case HTML:
456 fieldValueBuilder.setStringValue(html);
457 fieldValueBuilder.setType(ContentType.HTML);
458 break;
459 case ATOM:
460 fieldValueBuilder.setStringValue(atom);
461 fieldValueBuilder.setType(ContentType.ATOM);
462 break;
463 case DATE:
464 fieldValueBuilder.setStringValue(DateUtil.serializeDate(date));
465 fieldValueBuilder.setType(ContentType.DATE);
466 break;
467 case NUMBER:
468 DecimalFormat format = new DecimalFormat();
469 format.setDecimalSeparatorAlwaysShown(false);
470 format.setGroupingUsed(false);
471 fieldValueBuilder.setStringValue(format.format(number));
472 fieldValueBuilder.setType(ContentType.NUMBER);
473 break;
474 case GEO_POINT:
475 fieldValueBuilder.setGeo(geoPoint.copyToProtocolBuffer());
476 fieldValueBuilder.setType(ContentType.GEO);
477 break;
478 default:
479 throw new IllegalArgumentException(String.format("unknown field type %s", type));
483 DocumentPb.Field.Builder builder = DocumentPb.Field.newBuilder()
484 .setName(name)
485 .setValue(fieldValueBuilder);
486 return builder.build();
489 @Override
490 public String toString() {
491 return String.format("Field(name=%s%s, type=%s%s)",
492 name,
493 Util.fieldToString("value", valueToString()),
494 type.toString(),
495 Util.fieldToString("locale", locale));
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));
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
527 @Deprecated
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:
536 * <pre>
537 * Date d = ...
538 * Date yearMonthDay = Field.truncate(d, Calendar.DAY_OF_MONTH);
539 * </pre>
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
547 @Deprecated
548 public static Date truncate(Date date, int field) {
549 return DateUtil.truncate(date, field);