001/* 
002 * JKNIV, whinstone one contract to access your database.
003 * 
004 * Copyright (C) 2017, the original author or authors.
005 *
006 * This library is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 2.1 of the License.
010 * 
011 * This library is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 * 
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with this library; if not, write to the Free Software Foundation, Inc., 
018 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
019 */
020package net.sf.jkniv.whinstone.couchdb;
021
022import static net.sf.jkniv.whinstone.couchdb.statement.AllDocsQueryParams.KEY_limit;
023import static net.sf.jkniv.whinstone.couchdb.statement.AllDocsQueryParams.KEY_skip;
024
025import java.nio.charset.Charset;
026import java.util.Arrays;
027import java.util.List;
028
029import org.apache.http.Consts;
030import org.apache.http.client.methods.HttpPost;
031import org.apache.http.client.methods.HttpRequestBase;
032import org.apache.http.entity.StringEntity;
033
034import net.sf.jkniv.reflect.beans.PropertyAccess;
035import net.sf.jkniv.sqlegance.RepositoryException;
036import net.sf.jkniv.whinstone.Param;
037import net.sf.jkniv.whinstone.Queryable;
038import net.sf.jkniv.whinstone.couchdb.statement.QueryParam;
039import net.sf.jkniv.whinstone.params.ParameterNotFoundException;
040
041public class HttpBuilder
042{
043    private final String                  url;
044    private final String                  schema;
045    private final String                  hostContext;
046    private final Charset                 charset;
047    private RequestParams                 requestParams;
048    private CouchDbAuthenticate           auth;
049    // TODO refactor this lot of query params
050    private static final List<String>     KEY_PARAMS_ALLDOCS     = Arrays.asList("conflicts", "descending", "endkey",
051            "end_key", "endkey_docid", "end_key_doc_id", "include_docs", "inclusive_end", "key", "keys", "stale",
052            "startkey", "start_key", "startkey_docid", "start_key_doc_id", "update_seq");                            /*"limit","skip"*/
053    
054    private static final List<QueryParam> KEY_PARAMS_VIEW        = Arrays.asList(
055            new QueryParam("key", true),    //
056            new QueryParam("keys", true),   //
057            new QueryParam("startkey", true),//
058            new QueryParam("endkey", true), //
059            new QueryParam("descending"),   //
060            new QueryParam("group"),        //
061            new QueryParam("group_level"),  //
062            new QueryParam("startkey_docid"),//
063            new QueryParam("endkey_docid"), //
064            new QueryParam("limit"),        //
065            new QueryParam("skip"),         //
066            new QueryParam("descending"),   //
067            new QueryParam("stale"),        //
068            new QueryParam("reduce"),       //
069            new QueryParam("include_docs"), //
070            new QueryParam("inclusive_end"),//
071            new QueryParam("update_seq"));
072    
073    private static final List<QueryParam> KEY_PARAMS_GET         = Arrays.asList(new QueryParam("attachments"),
074            new QueryParam("att_encoding_info"), new QueryParam("atts_since"), new QueryParam("conflicts"),
075            new QueryParam("deleted_conflicts"), new QueryParam("latest"), new QueryParam("local_seq"),
076            new QueryParam("meta"), new QueryParam("open_revs"), new QueryParam("rev"), new QueryParam("revs"),
077            new QueryParam("revs_info"));
078    
079    private static final List<QueryParam> KEY_PARAMS_SAVE        = Arrays.asList(new QueryParam("batch"),
080            new QueryParam("new_edits"));
081    
082    private static final List<String>     KEY_PARAMS_UNSUPPORTED = Arrays.asList("attachments");
083    
084    public HttpBuilder(CouchDbAuthenticate auth, String url, String schema, RequestParams requestParams)
085    {
086        super();
087        this.auth = auth;
088        this.url = url;
089        this.schema = schema;
090        this.hostContext = getHostContext();
091        this.requestParams = requestParams;
092        this.charset = Charset.forName("UTF-8");
093    }
094    
095    public void setHeader(HttpRequestBase http)
096    {
097        String token = auth.getCookieSession();
098        
099        if (auth.isExpired())
100            token = auth.authenticate();
101        
102        http.addHeader("Cookie", token);
103        requestParams.setHeader(http);
104    }
105    
106    /**
107     * Return the URL for _design of documents.
108     * <p>
109     * <code>
110     * http://{host}:{port}/{schema}/_design/
111     * </code>
112     * 
113     * @return URL follow the pattern http://{host}:{port}/{schema}/_design/ 
114     */
115    public String getUrlForDesign()
116    {
117        return this.hostContext + "_design/";
118    }
119    
120    /**
121     * Return the HTTP POST for _find request.
122     * <p>
123     * <code>
124     * http://{host}:{port}/{schema}/_find/
125     * </code>
126     * 
127     * @param bodyStr body from HTTP POST request
128     * @return URL follow the pattern http://{host}:{port}/{schema}/_find/ with {@code bodyStr} as body content 
129     */
130    public HttpPost newFind(String bodyStr)
131    {
132        HttpPost httpPost = null;
133        try
134        {
135            StringEntity body = new StringEntity(bodyStr, Consts.UTF_8); // TODO config charset for HTTP body
136            String fullUrl = this.hostContext + "_find";
137            httpPost = new HttpPost(fullUrl);
138            requestParams.setHeader(httpPost);
139            httpPost.setEntity(body);
140        }
141        catch (Exception ex)
142        {
143            throw new RepositoryException("Cannot build new URI [" + this.hostContext + "_find]");
144        }
145        return httpPost;
146    }
147    
148    /**
149     * Return the HTTP POST for _find request.
150     * <p>
151     * <code>
152     * http://{host}:{port}/{schema}/_find/
153     * </code>
154     * 
155     * @return URL follow the pattern http://{host}:{port}/{schema}/_find/ without body content
156     */
157    public HttpPost newFind()
158    {
159        HttpPost http = null;
160        try
161        {
162            String fullUrl = this.hostContext + "_find";
163            http = new HttpPost(fullUrl);
164            requestParams.setHeader(http);
165        }
166        catch (Exception ex)
167        {
168            throw new RepositoryException("Cannot build new URI [" + this.hostContext + "_find]");
169        }
170        return http;
171    }
172    
173    public String getUrlForView(Queryable queryable)
174    {
175        checkUnsupportedQueryParams(queryable);
176        StringBuilder urlParams = new StringBuilder("?");
177        StringBuilder urlAllDocs = new StringBuilder(this.hostContext + "_design/" + queryable.getName());
178        try
179        {
180            // https://docs.couchdb.org/en/stable/ddocs/views/pagination.html
181            // 3.2.5.5. Paging (Alternate Method)
182            // curl -X GET 'http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5&skip=5'
183            if (queryable.isPaging())
184                urlParams.append("limit="+queryable.getMax()+"&skip="+queryable.getOffset());
185                
186            for (QueryParam k : KEY_PARAMS_VIEW)
187            {
188                Param param = getProperty(queryable, k.name());
189                if (param.getValue() != null)
190                {
191                    if (urlParams.length() > 1)
192                        urlParams.append("&" + k.name() + "=" + k.getValue(param.getValue()));
193                    else
194                        urlParams.append(k.name() + "=" + k.getValue(param.getValue()));
195                }
196            }
197        }
198        catch (Exception ex)
199        {
200            throw new RepositoryException("Cannot build new URI [" + this.hostContext + "_view]");
201        }
202        return urlAllDocs.toString() + urlParams.toString();
203    }
204    
205    public String getUrlForGet(Queryable queryable)
206    {
207        checkUnsupportedQueryParams(queryable);
208        StringBuilder urlParams = new StringBuilder("?");
209        StringBuilder urlGet = new StringBuilder(this.hostContext);
210        PropertyAccess accessId = queryable.getDynamicSql().getSqlDialect().getAccessId();
211        if (queryable.isTypeOfBasic())
212            urlGet.append(queryable.getParams());// FIXME design docid is Date, Calendar, how to parser
213        else
214        {
215            Param param = getProperty(queryable, accessId.getFieldName());
216            if (param.getValue() != null)
217                urlGet.append(param.getValue());
218            else
219                throw new RepositoryException("Cannot lookup id using Property Access ["+accessId+"] from [" + queryable + "]");
220        }
221        
222        try
223        {
224            for (QueryParam k : KEY_PARAMS_GET)
225            {
226                Param param = getProperty(queryable, k.name());
227                if (param.getValue() != null)
228                {
229                    if (urlParams.length() > 1)
230                        urlParams.append("&" + k.name() + "=" + k.getValue(param.getValue()));
231                    else
232                        urlParams.append(k.name() + "=" + k.getValue(param.getValue()));
233                }
234            }
235        }
236        catch (Exception ex)
237        {
238            throw new RepositoryException("Cannot build new URI [" + urlGet.toString() + urlParams.toString() + "]");
239        }
240        return urlGet.toString() + urlParams.toString();
241    }
242    
243    public String getUrlForAddOrUpdateOrDelete(Queryable queryable)
244    {
245        checkUnsupportedQueryParams(queryable);
246        StringBuilder urlSave = new StringBuilder(this.hostContext);
247        PropertyAccess accessId = queryable.getDynamicSql().getSqlDialect().getAccessId();
248        Param param = getProperty(queryable, accessId.getFieldName());
249        if (param.getValue() != null)
250            urlSave.append(param.getValue());
251           
252        return urlSave.toString();
253    }
254
255    public String getUrlForAllDocs(Queryable queryable)
256    {
257        checkUnsupportedQueryParams(queryable);
258        StringBuilder urlParams = new StringBuilder("?");
259        StringBuilder urlAllDocs = new StringBuilder(this.hostContext + "_all_docs");
260        try
261        {
262            if (queryable.isPaging())
263                urlParams.append(KEY_limit + "=" + queryable.getMax() + "&" + KEY_skip + "=" + queryable.getOffset());
264            
265            for (String k : KEY_PARAMS_ALLDOCS)
266            {
267                Param v = getProperty(queryable, k);
268                if (v.getValue() != null)
269                {
270                    if (urlParams.length() > 1)
271                        urlParams.append("&" + k + "=" + v.getValue());
272                    else
273                        urlParams.append(k + "=" + v.getValue());
274                }
275            }
276        }
277        catch (Exception ex)
278        {
279            throw new RepositoryException("Cannot build new URI [" + this.hostContext + "_all_docs]");
280        }
281        return urlAllDocs.toString() + urlParams.toString();
282    }
283    
284    public String getUrlForBulk()
285    {
286        StringBuilder urlSave = new StringBuilder(this.hostContext);
287        urlSave.append("_bulk_docs");
288        return urlSave.toString();
289    }
290
291    
292    public String getHostContext()
293    {
294        String hostContext = this.url + "/" + this.schema;
295        if (this.url.endsWith("/"))
296            hostContext = this.url + this.schema;
297        
298        return hostContext + "/";
299    }
300    
301    public Param getProperty(Queryable queryable, String name)
302    {
303        Param v = null;
304        try
305        {
306            v = queryable.getProperty(name);
307        }
308        catch (ParameterNotFoundException ignore) { /* parameter not exixts */}
309        return v == null ? new Param() : v;
310    }
311    
312    private void checkUnsupportedQueryParams(Queryable queryable)
313    {
314        for (String k : KEY_PARAMS_UNSUPPORTED)
315        {
316            Param v = getProperty(queryable, k);
317            if (v.getValue() != null)
318                throw new RepositoryException("Query Parameters [" + k + "] isn't supported yet!");
319        }
320    }
321}