Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / search / Facet.java
blob01f73762e4c2eda0d8a2c95d58434b835bddbbdf
1 package com.google.appengine.api.search;
3 import com.google.appengine.api.search.checkers.FacetChecker;
4 import com.google.appengine.api.search.checkers.Preconditions;
5 import com.google.apphosting.api.search.DocumentPb;
6 import com.google.apphosting.api.search.DocumentPb.FacetValue;
7 import com.google.apphosting.api.search.DocumentPb.FacetValue.ContentType;
9 import java.io.Serializable;
10 import java.util.Objects;
12 /**
13 * A {@code Facet} can be used to categorize a {@link Document}. It is not a {@link Field}.
14 * <p>
15 * Search results can contain facets for the extended result set and their value frequency.
16 * For example, if a search query is related to "wine", then facets could be "color" with values
17 * of "red" and "white", and "year" with values of "2000" and "2005".
18 * <p>
19 * Each facet has a name and exactly one value: atom or number. Facet name lengths are between 1 and
20 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_NAME_LENGTH} characters,
21 * and atoms are
22 * limited to {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_ATOM_LENGTH}
23 * characters. Numbers must be between
24 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MINIMUM_NUMBER_VALUE} and
25 * {@link com.google.appengine.api.search.checkers.SearchApiLimits#MAXIMUM_NUMBER_VALUE}.
27 public final class Facet implements Serializable {
29 /**
30 * Creates and returns an atom facet with the given {@code name} and {@code value}.
32 * @return an instance of {@link Facet}.
33 * @throws IllegalArgumentException if the facet name or value are invalid.
35 public static Facet withAtom(String name, String value) {
36 return new Facet(name, value, null);
39 /**
40 * Creates and returns a number facet with the given {@code name} and {@code value}.
42 * @return an instance of {@link Facet}.
43 * @throws IllegalArgumentException if the facet name or value are invalid.
45 public static Facet withNumber(String name, Double value) {
46 return new Facet(name, null, value);
49 private static final long serialVersionUID = 3297968709406212335L;
51 private final String name;
52 private String atom;
53 private Double number;
55 private Facet(String name, String atom, Double number) {
56 this.name = name;
57 this.atom = atom;
58 this.number = number;
59 checkValid();
62 /**
63 * Returns the name of the facet.
65 public String getName() {
66 return name;
69 /**
70 * Returns the atomic value of the facet. Returns null if the value is not atomic.
72 public String getAtom() {
73 return atom;
76 /**
77 * Returns the numeric value of the facet. Returns null if the value is not numeric.
79 public Double getNumber() {
80 return number;
83 @Override
84 public int hashCode() {
85 return Objects.hash(name, atom, number);
88 @Override
89 public boolean equals(Object object) {
90 if (object == this) {
91 return true;
93 if (!(object instanceof Facet)) {
94 return false;
96 Facet facet = (Facet) object;
97 return Util.equalObjects(name, facet.name)
98 && Util.equalObjects(atom, facet.atom)
99 && Util.equalObjects(number, facet.number);
103 * Checks whether the facet is valid, specifically,
104 * whether the facet name, value are valid.
105 * Also that at most one value: atom or number is set.
107 * @throws IllegalArgumentException if facet name, atom, or number is invalid
109 private void checkValid() {
110 FacetChecker.checkFacetName(name);
111 if (atom != null) {
112 if (number != null) {
113 throw new IllegalArgumentException(
114 String.format("both atom and number are set for facet %s", name));
116 FacetChecker.checkAtom(atom);
117 } else if (number != null) {
118 FacetChecker.checkNumber(number);
119 } else {
120 throw new IllegalArgumentException(
121 String.format("neither atom nor number is set for facet %s", name));
126 * Creates and returns a facet reflecting a {@link DocumentPb.Facet} protocol message.
128 * @throws SearchException if the facet contains an invalid name, atom or number.
130 static Facet withProtoMessage(DocumentPb.Facet facet) {
131 FacetValue value = facet.getValue();
132 switch (value.getType()) {
133 case ATOM:
134 try {
135 return withAtom(facet.getName(), value.getStringValue());
136 } catch (IllegalArgumentException e) {
137 throw new SearchException(
138 String.format("Failed to create facet %s from protocol message: %s", facet.getName(),
139 e.getMessage()));
141 case NUMBER:
142 try {
143 return withNumber(facet.getName(), Facet.stringToNumber(value.getStringValue()));
144 } catch (NumberFormatException e) {
145 throw new SearchException("Failed to parse double: " + value.getStringValue());
146 } catch (IllegalArgumentException e) {
147 throw new SearchException(
148 String.format("Failed to create facet %s from protocol message: %s", facet.getName(),
149 e.getMessage()));
151 default:
152 throw new SearchException(
153 String.format("unknown facet type %s for facet %s",
154 String.valueOf(value.getType()), facet.getName()));
159 * Copies a {@link Facet} object into a {@link com.google.apphosting.api.search.DocumentPb.Facet}
160 * protocol buffer.
162 * @return the facet protocol buffer copy of this facet object
164 DocumentPb.Facet copyToProtocolBuffer() {
165 DocumentPb.FacetValue.Builder facetValueBuilder = DocumentPb.FacetValue.newBuilder();
166 if (atom != null) {
167 facetValueBuilder.setType(ContentType.ATOM);
168 facetValueBuilder.setStringValue(atom);
169 } else if (number != null) {
170 facetValueBuilder.setType(ContentType.NUMBER);
171 facetValueBuilder.setStringValue(numberToString(number));
173 DocumentPb.Facet.Builder builder = DocumentPb.Facet.newBuilder()
174 .setName(name)
175 .setValue(facetValueBuilder);
176 return builder.build();
179 @Override
180 public String toString() {
181 return new Util.ToStringHelper("Facet")
182 .addField("name", name)
183 .addField("atom", atom)
184 .addField("number", number)
185 .finish();
189 * Converts a double number into a string acceptable by faceting backend as a number.
191 * @throws IllegalArgumentException if the {@code number} is NaN.
193 static String numberToString(double number) {
194 Preconditions.checkArgument(!Double.isNaN(number), "should be a number.");
195 return Double.toString(number);
199 * Converts a string with faceting backend format into double number.
201 * @throws NullPointerException if the string is null
202 * @throws NumberFormatException if the string does not contain a parsable double.
204 static Double stringToNumber(String string) {
205 return Double.parseDouble(string);