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