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.commands;
021
022import java.io.IOException;
023import java.util.Map;
024
025import org.apache.http.client.methods.CloseableHttpResponse;
026import org.apache.http.client.methods.HttpPost;
027import org.apache.http.client.methods.HttpPut;
028import org.apache.http.client.methods.HttpRequestBase;
029import org.apache.http.impl.client.CloseableHttpClient;
030import org.apache.http.impl.client.HttpClients;
031import org.apache.http.util.EntityUtils;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import net.sf.jkniv.reflect.beans.ObjectProxy;
036import net.sf.jkniv.reflect.beans.ObjectProxyFactory;
037import net.sf.jkniv.reflect.beans.PropertyAccess;
038import net.sf.jkniv.sqlegance.Insertable;
039import net.sf.jkniv.sqlegance.RepositoryException;
040import net.sf.jkniv.whinstone.Queryable;
041import net.sf.jkniv.whinstone.couchdb.HttpBuilder;
042
043/**
044 * <pre>
045 * 
046 * http://docs.couchdb.org/en/2.0.0/api/document/common.html
047 * 
048 * PUT /{db}/{docid}
049 *
050 *  The PUT method creates a new named document, or creates a new revision of the existing document. 
051 *  Unlike the POST /{db}, you must specify the document ID in the request URL.
052 *  
053 *  Parameters: 
054 *
055 *      db – Database name
056 *      docid – Document ID
057 *
058 *  Request Headers:
059 *      
060 * 
061 *      Accept –
062 *          application/json
063 *          text/plain
064 *      Content-Type – application/json
065 *      If-Match – Document’s revision. Alternative to rev query parameter
066 *      X-Couch-Full-Commit – Overrides server’s commit policy. Possible values are: false and true. Optional
067 * 
068 *  Query Parameters:
069 *      
070 *
071 *      batch (string) – Stores document in batch mode. Possible values: ok. Optional
072 *      new_edits (boolean) – Prevents insertion of a conflicting document. 
073 *                            Possible values: true (default) and false. If false, a well-formed _rev must be included 
074 *                            in the document. new_edits=false is used by the replicator to insert documents into 
075 *                            the target database even if that leads to the creation of conflicts. Optional
076 *
077 *  Response Headers:
078 *      
079 *
080 *      Content-Type –
081 *                     application/json
082 *                     text/plain; charset=utf-8
083 *      ETag – Quoted document’s new revision
084 *      Location – Document URI
085 *
086 *  Response JSON Object:
087 *      
088 *
089 *      id (string) – Document ID
090 *      ok (boolean) – Operation status
091 *      rev (string) – Revision MVCC token
092 *
093 *  Status Codes:   
094 * 
095 *      201 Created – Document created and stored on disk
096 *      202 Accepted – Document data accepted, but not yet stored on disk
097 *      400 Bad Request – Invalid request body or parameters
098 *      401 Unauthorized – Write privileges required
099 *      404 Not Found – Specified database or document ID doesn’t exists
100 *      409 Conflict – Document with the specified ID already exists or specified revision is not 
101 *                     latest for target document
102 *
103 * </pre>
104 * 
105 * @author Alisson Gomes
106 * @since 0.6.0
107 *
108 */
109public class AddCommand extends AbstractCommand implements CouchCommand
110{
111    private static final Logger LOG = LoggerFactory.getLogger(AddCommand.class);
112    private static final Logger LOGSQL = net.sf.jkniv.whinstone.couchdb.LoggerFactory.getLogger();
113    private Queryable           queryable;
114    private HttpBuilder         httpBuilder;
115    
116    public AddCommand(HttpBuilder httpBuilder, Queryable queryable)
117    {
118        super();
119        this.httpBuilder = httpBuilder;
120        this.queryable = queryable;
121        this.method = HttpMethod.PUT;
122        this.body = JsonMapper.mapper(queryable.getParams());
123    }
124    
125    @SuppressWarnings("unchecked")
126    @Override
127    public <T> T execute()
128    {
129        String json = null;
130        CloseableHttpResponse response = null;
131        //Map<String, Object> answer = null;
132        T answer = null;
133        try
134        {
135            CloseableHttpClient httpclient = HttpClients.createDefault();
136            String url = httpBuilder.getUrlForAddOrUpdateOrDelete(queryable);
137            HttpRequestBase http = null;
138            if (url.equals(httpBuilder.getHostContext()))
139            {
140                http = asPost().newHttp(url);
141                ((HttpPost) http).setEntity(getEntity());
142            }
143            else
144            {
145                http = asPut().newHttp(url);
146                ((HttpPut) http).setEntity(getEntity());
147            }
148            // FIXME supports header request for PUT commands -> Headers: "If-Match", "X-Couch-Full-Commit"
149            httpBuilder.setHeader(http);
150            printRequest(http);
151            response = httpclient.execute(http);
152            json = EntityUtils.toString(response.getEntity());
153            printResponse(response, json);
154            int statusCode = response.getStatusLine().getStatusCode();
155            if (isCreated(statusCode))
156            {
157                processResponse(queryable, json);
158                answer = (T) Integer.valueOf("1");
159            }
160            else if (isAccepted(statusCode))
161            {
162                LOG.info("Document data accepted, but not yet stored on disk");
163                processResponse(queryable, json);
164                answer = (T) Integer.valueOf("1");
165            }
166            else if (isNotFound(statusCode))
167            {
168                answer = (T) Integer.valueOf("0");
169                // 204 No Content, 304 Not Modified, 205 Reset Content, 404 Not Found
170                LOG.warn(errorFormat(http, response.getStatusLine(), json));
171            }
172            else
173            {
174                Map<String, String> result = JsonMapper.mapper(json, Map.class);
175                String reason = result.get("reason");
176                LOG.error(errorFormat(http, response.getStatusLine(), json));
177                throw new RepositoryException(response.getStatusLine().toString() + ", " + reason);
178            }
179            //commandHandler.postCommit();
180        }
181        catch (Exception e) // ClientProtocolException | JsonParseException | JsonMappingException | IOException
182        {
183            //commandHandler.postException();
184            handlerException.handle(e);
185        }
186        finally
187        {
188            if (response != null)
189            {
190                try
191                {
192                    response.close();
193                }
194                catch (IOException e)
195                {
196                    handlerException.handle(e);
197                }
198            }
199        }
200        return answer;
201    }
202    
203    @SuppressWarnings("unchecked")
204    private void processResponse(Queryable queryable, String json)
205    {
206        Map<String, Object> response = JsonMapper.mapper(json, Map.class);
207        Insertable sql = queryable.getDynamicSql().asInsertable();
208        Object params = queryable.getParams();
209        ObjectProxy<?> proxy = ObjectProxyFactory.of(params);
210        PropertyAccess accessId = queryable.getDynamicSql().getSqlDialect().getAccessId();
211        PropertyAccess accessRev = queryable.getDynamicSql().getSqlDialect().getAccessRevision();
212        String id = (String) response.get(accessId.getFieldName());
213        String rev = (String) response.get(accessRev.getFieldName());
214        if (sql.isAutoGenerateKey())
215        {
216            String properName = sql.getAutoGeneratedKey().getProperties();
217            injectAutoIdentity(proxy, params, id, rev, properName, accessId, accessRev);
218        }
219        else
220        {
221            injectIdentity(proxy, params, id, rev, accessId, accessRev);
222        }
223    }
224    
225    @Override
226    public String getBody()
227    {
228        return this.body;
229    }
230    
231    @Override
232    public HttpMethod asPut()
233    {
234        this.method = HttpMethod.PUT;
235        return this.method;
236    }
237    
238    @Override
239    public HttpMethod asPost()
240    {
241        this.method = HttpMethod.POST;
242        return this.method;
243    }
244    
245}