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