1
+ /*
2
+ * Copyright (c) 2016 Algolia
3
+ * http://www.algolia.com/
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in
13
+ * all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ * THE SOFTWARE.
22
+ */
23
+
24
+ package com .algolia .search .saas ;
25
+
26
+ import android .support .annotation .NonNull ;
27
+ import android .support .annotation .Nullable ;
28
+ import android .text .TextUtils ;
29
+
30
+ import org .json .JSONArray ;
31
+ import org .json .JSONException ;
32
+
33
+ import java .io .UnsupportedEncodingException ;
34
+ import java .net .URLDecoder ;
35
+ import java .net .URLEncoder ;
36
+ import java .util .Map ;
37
+ import java .util .TreeMap ;
38
+
39
+
40
+ // ----------------------------------------------------------------------
41
+ // IMPLEMENTATION NOTES
42
+ // ----------------------------------------------------------------------
43
+ // The query parameters are stored as an untyped map of strings.
44
+ // This class provides:
45
+ // - low-level accessors to the untyped parameters;
46
+ // - higher-level, typed accessors.
47
+ // The latter simply serialize their values into the untyped map and parse
48
+ // them back from it.
49
+ // ----------------------------------------------------------------------
50
+
51
+ /**
52
+ * An abstract search query.
53
+ */
54
+ public abstract class AbstractQuery {
55
+
56
+ // ----------------------------------------------------------------------
57
+ // Types
58
+ // ----------------------------------------------------------------------
59
+
60
+ /**
61
+ * A pair of (latitude, longitude).
62
+ * Used in geo-search.
63
+ */
64
+ public static final class LatLng {
65
+ public final double lat ;
66
+ public final double lng ;
67
+
68
+ public LatLng (double lat , double lng ) {
69
+ this .lat = lat ;
70
+ this .lng = lng ;
71
+ }
72
+
73
+ @ Override
74
+ public boolean equals (Object other ) {
75
+ return other != null && other instanceof LatLng
76
+ && this .lat == ((LatLng )other ).lat && this .lng == ((LatLng )other ).lng ;
77
+ }
78
+
79
+ @ Override
80
+ public int hashCode () {
81
+ return (int )Math .round (lat * lng % Integer .MAX_VALUE );
82
+ }
83
+
84
+ /**
85
+ * Parse a `LatLng` from its string representation.
86
+ *
87
+ * @param value A string representation of a (latitude, longitude) pair, in the format `12.345,67.890`
88
+ * (number of digits may vary).
89
+ * @return A `LatLng` instance describing the given geolocation, or `null` if `value` is `null` or does not
90
+ * represent a valid geolocation.
91
+ */
92
+ @ Nullable public static LatLng parse (String value ) {
93
+ if (value == null ) {
94
+ return null ;
95
+ }
96
+ String [] components = value .split ("," );
97
+ if (components .length != 2 ) {
98
+ return null ;
99
+ }
100
+ try {
101
+ return new LatLng (Double .valueOf (components [0 ]), Double .valueOf (components [1 ]));
102
+ } catch (NumberFormatException e ) {
103
+ return null ;
104
+ }
105
+ }
106
+ }
107
+
108
+ // ----------------------------------------------------------------------
109
+ // Fields
110
+ // ----------------------------------------------------------------------
111
+
112
+ /** Query parameters, as an untyped key-value array. */
113
+ // NOTE: Using a tree map to have parameters sorted by key on output.
114
+ private Map <String , String > parameters = new TreeMap <>();
115
+
116
+ // ----------------------------------------------------------------------
117
+ // Construction
118
+ // ----------------------------------------------------------------------
119
+
120
+ /**
121
+ * Construct an empty query.
122
+ */
123
+ protected AbstractQuery () {
124
+ }
125
+
126
+ /**
127
+ * Clone an existing query.
128
+ * @param other The query to be cloned.
129
+ */
130
+ protected AbstractQuery (@ NonNull AbstractQuery other ) {
131
+ parameters = new TreeMap <>(other .parameters );
132
+ }
133
+
134
+ // ----------------------------------------------------------------------
135
+ // Equality
136
+ // ----------------------------------------------------------------------
137
+
138
+ @ Override
139
+ public boolean equals (Object other ) {
140
+ return other != null && other instanceof AbstractQuery && this .parameters .equals (((AbstractQuery )other ).parameters );
141
+ }
142
+
143
+ @ Override
144
+ public int hashCode () {
145
+ return parameters .hashCode ();
146
+ }
147
+
148
+ // ----------------------------------------------------------------------
149
+ // Misc.
150
+ // ----------------------------------------------------------------------
151
+
152
+ /**
153
+ * Obtain a debug representation of this query.
154
+ * To get the raw query URL part, please see {@link #build()}.
155
+ * @return A debug representation of this query.
156
+ */
157
+ @ Override public @ NonNull String toString () {
158
+ return String .format ("%s{%s}" , this .getClass ().getSimpleName (), this .build ());
159
+ }
160
+
161
+ // ----------------------------------------------------------------------
162
+ // Parsing/serialization
163
+ // ----------------------------------------------------------------------
164
+
165
+ /**
166
+ * Build the URL query parameter string representing this object.
167
+ * @return A string suitable for use inside the query part of a URL (i.e. after the question mark).
168
+ */
169
+ public @ NonNull String build () {
170
+ StringBuilder stringBuilder = new StringBuilder ();
171
+ try {
172
+ for (Map .Entry <String , String > entry : parameters .entrySet ()) {
173
+ String key = entry .getKey ();
174
+ if (stringBuilder .length () > 0 )
175
+ stringBuilder .append ('&' );
176
+ stringBuilder .append (urlEncode (key ));
177
+ String value = entry .getValue ();
178
+ if (value != null ) {
179
+ stringBuilder .append ('=' );
180
+ stringBuilder .append (urlEncode (value ));
181
+ }
182
+ }
183
+ } catch (UnsupportedEncodingException e ) {
184
+ throw new RuntimeException (e ); // should never happen: UTF-8 is always supported
185
+ }
186
+ return stringBuilder .toString ();
187
+ }
188
+
189
+ static private String urlEncode (String value ) throws UnsupportedEncodingException {
190
+ // NOTE: We prefer to have space encoded as `%20` instead of `+`, so we patch `URLEncoder`'s behaviour.
191
+ // This works because `+` itself is percent-escaped (into `%2B`).
192
+ return URLEncoder .encode (value , "UTF-8" ).replace ("+" , "%20" );
193
+ }
194
+
195
+ /**
196
+ * Parse a URL query parameter string and store the resulting parameters into this query.
197
+ * @param queryParameters URL query parameter string.
198
+ */
199
+ protected void parseFrom (@ NonNull String queryParameters ) {
200
+ try {
201
+ String [] parameters = queryParameters .split ("&" );
202
+ for (String parameter : parameters ) {
203
+ String [] components = parameter .split ("=" );
204
+ if (components .length < 1 || components .length > 2 )
205
+ continue ; // ignore invalid values
206
+ String name = URLDecoder .decode (components [0 ], "UTF-8" );
207
+ String value = components .length >= 2 ? URLDecoder .decode (components [1 ], "UTF-8" ) : null ;
208
+ set (name , value );
209
+ } // for each parameter
210
+ } catch (UnsupportedEncodingException e ) {
211
+ // Should never happen since UTF-8 is one of the default encodings.
212
+ throw new RuntimeException (e );
213
+ }
214
+ }
215
+
216
+ protected static Boolean parseBoolean (String value ) {
217
+ if (value == null ) {
218
+ return null ;
219
+ }
220
+ if (value .trim ().toLowerCase ().equals ("true" )) {
221
+ return true ;
222
+ }
223
+ Integer intValue = parseInt (value );
224
+ return intValue != null && intValue != 0 ;
225
+ }
226
+
227
+ protected static Integer parseInt (String value ) {
228
+ if (value == null ) {
229
+ return null ;
230
+ }
231
+ try {
232
+ return Integer .parseInt (value .trim ());
233
+ } catch (NumberFormatException e ) {
234
+ return null ;
235
+ }
236
+ }
237
+
238
+ protected static String buildJSONArray (String [] values ) {
239
+ JSONArray array = new JSONArray ();
240
+ for (String value : values ) {
241
+ array .put (value );
242
+ }
243
+ return array .toString ();
244
+ }
245
+
246
+ protected static String [] parseArray (String string ) {
247
+ if (string == null ) {
248
+ return null ;
249
+ }
250
+ // First try to parse JSON notation.
251
+ try {
252
+ JSONArray array = new JSONArray (string );
253
+ String [] result = new String [array .length ()];
254
+ for (int i = 0 ; i < result .length ; ++i ) {
255
+ result [i ] = array .optString (i );
256
+ }
257
+ return result ;
258
+ }
259
+ // Otherwise parse as a comma-separated list.
260
+ catch (JSONException e ) {
261
+ return string .split ("," );
262
+ }
263
+ }
264
+
265
+ protected static String buildCommaArray (String [] values ) {
266
+ return TextUtils .join ("," , values );
267
+ }
268
+
269
+ protected static String [] parseCommaArray (String string ) {
270
+ return string == null ? null : string .split ("," );
271
+ }
272
+
273
+ /**
274
+ * @deprecated Please use {@link LatLng#parse(String)} instead.
275
+ */
276
+ @ Nullable public static LatLng parseLatLng (String value ) {
277
+ return LatLng .parse (value );
278
+ }
279
+
280
+ // ----------------------------------------------------------------------
281
+ // Low-level (untyped) accessors
282
+ // ----------------------------------------------------------------------
283
+
284
+ /**
285
+ * Set a parameter in an untyped fashion.
286
+ * This low-level accessor is intended to access parameters that this client does not yet support.
287
+ * @param name The parameter's name.
288
+ * @param value The parameter's value, or null to remove it.
289
+ * It will first be converted to a String by the `toString()` method.
290
+ * @return This instance (used to chain calls).
291
+ */
292
+ public @ NonNull AbstractQuery set (@ NonNull String name , @ Nullable Object value ) {
293
+ if (value == null ) {
294
+ parameters .remove (name );
295
+ } else {
296
+ parameters .put (name , value .toString ());
297
+ }
298
+ return this ;
299
+ }
300
+
301
+ /**
302
+ * Get a parameter in an untyped fashion.
303
+ * @param name The parameter's name.
304
+ * @return The parameter's value, or null if a parameter with the specified name does not exist.
305
+ */
306
+ public @ Nullable String get (@ NonNull String name ) {
307
+ return parameters .get (name );
308
+ }
309
+ }
0 commit comments