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.HttpDelete;
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 * DELETE /{db}/{docid}
046 *
047 *  Marks the specified document as deleted by adding a field _deleted with the value true. Documents with this field will not be returned within requests anymore, but stay in the database. You must supply the current (latest) revision, either by using the rev parameter or by using the If-Match header to specify the revision.
048 *
049 *  Note
050 *
051 *  CouchDB doesn’t completely delete the specified document. Instead, it leaves a tombstone with very basic information about the document. The tombstone is required so that the delete action can be replicated across databases.
052 *
053 *  See also
054 *
055 *  Retrieving Deleted Documents
056 *  Parameters: 
057 *
058 *      db – Database name
059 *      docid – Document ID
060 *
061 *  Request Headers:
062 *      
063 *
064 *      Accept –
065 *          application/json
066 *          text/plain
067 *      If-Match – Document’s revision. Alternative to rev query parameter
068 *      X-Couch-Full-Commit – Overrides server’s commit policy. Possible values are: false and true. Optional
069 *
070 *  Query Parameters:
071 *      
072 *
073 *      rev (string) – Actual document’s revision
074 *      batch (string) – Stores document in batch mode Possible values: ok. Optional
075 *
076 *  Response Headers:
077 *      
078 *
079 *      Content-Type –
080 *          application/json
081 *          text/plain; charset=utf-8
082 *      ETag – Double quoted document’s new revision
083 *
084 *  Response JSON Object:
085 *      
086 *
087 *      id (string) – Document ID
088 *      ok (boolean) – Operation status
089 *      rev (string) – Revision MVCC token
090 *
091 *  Status Codes:   
092 *
093 *      200 OK – Document successfully removed
094 *      202 Accepted – Request was accepted, but changes are not yet stored on disk
095 *      400 Bad Request – Invalid request body or parameters
096 *      401 Unauthorized – Write privileges required
097 *      404 Not Found – Specified database or document ID doesn’t exists
098 *      409 Conflict – Specified revision is not the latest for target document
099 *
100 *  Request:
101 *
102 * </pre>
103 * 
104 * @author Alisson Gomes
105 * @since 0.6.0
106 *
107 */
108public class DeleteCommand extends AbstractCommand implements CouchCommand
109{
110    private static final Logger LOG = LoggerFactory.getLogger(DeleteCommand.class);
111    private static final Logger LOGSQL = net.sf.jkniv.whinstone.couchdb.LoggerFactory.getLogger();
112    private Queryable           queryable;
113    private HttpBuilder         httpBuilder;
114    
115    public DeleteCommand(HttpBuilder httpBuilder, Queryable queryable)
116    {
117        super();
118        this.httpBuilder = httpBuilder;
119        this.queryable = queryable;
120        this.method = HttpMethod.PUT;
121        this.body = JsonMapper.mapper(queryable.getParams());
122    }
123    
124    @SuppressWarnings("unchecked")
125    @Override
126    public <T> T execute()
127    {
128        String json = null;
129        CloseableHttpResponse response = null;
130        //Map<String, Object> answer = null;
131        T answer = null;
132        try
133        {
134            CloseableHttpClient httpclient = HttpClients.createDefault();
135            String url = httpBuilder.getUrlForAddOrUpdateOrDelete(queryable);
136            HttpDelete http = null;
137            http = (HttpDelete)asDelete().newHttp(url);
138            //http.setEntity( getEntity() );
139            
140            // FIXME supports header request for PUT commands -> Headers: "If-Match", "X-Couch-Full-Commit"
141            httpBuilder.setHeader(http);
142            http.addHeader("If-Match", getRevision(queryable));
143            printRequest(http);
144            response = httpclient.execute(http);
145            json = EntityUtils.toString(response.getEntity());
146            printResponse(response, json);
147
148            int statusCode = response.getStatusLine().getStatusCode();
149            if (isOk(statusCode))
150            {
151                processResponse(queryable, json);
152                answer = (T)Integer.valueOf("1");
153            }
154            else if (isAccepted(statusCode))
155            {
156                LOG.info("Document data accepted, but not yet stored on disk");
157                processResponse(queryable, json);
158                answer = (T)Integer.valueOf("1");
159            }
160            else if (isNotFound(statusCode))
161            {
162                answer = (T)Integer.valueOf("0");
163                // 204 No Content, 304 Not Modified, 205 Reset Content, 404 Not Found
164                LOG.warn(errorFormat(http, response.getStatusLine(), json));
165            }
166            else
167            {
168                Map<String,String> result = JsonMapper.mapper(json, Map.class);
169                String reason = result.get("reason");
170                LOG.error(errorFormat(http, response.getStatusLine(), json));
171                throw new RepositoryException(response.getStatusLine().toString() +", "+ reason);
172            }
173            //commandHandler.postCommit();
174        }
175        catch (Exception e) // ClientProtocolException | JsonParseException | JsonMappingException | IOException
176        {
177            //commandHandler.postException();
178            handlerException.handle(e);
179        }
180        finally
181        {
182            if (response != null)
183            {
184                try
185                {
186                    response.close();
187                }
188                catch (IOException e)
189                {
190                    handlerException.handle(e);
191                }
192            }
193        }
194        return answer;
195    }
196    
197    private void processResponse(Queryable queryable, String json)
198    {
199        Map<String, Object> response = JsonMapper.mapper(json, Map.class);
200        //Updateable sql = queryable.getDynamicSql().asUpdateable();
201        Object params = queryable.getParams();
202        ObjectProxy<?> proxy = ObjectProxyFactory.of(params);
203        PropertyAccess accessId = queryable.getDynamicSql().getSqlDialect().getAccessId();
204        PropertyAccess accessRev= queryable.getDynamicSql().getSqlDialect().getAccessRevision();
205        String id = (String) response.get(accessId.getFieldName());
206        String rev = (String) response.get(accessRev.getFieldName());
207        if (params instanceof Map)
208        {
209            if (!((Map) params).containsKey(accessId.getFieldName()))
210                ((Map) params).put(accessId.getFieldName(), id);
211
212            ((Map) params).put(accessRev.getFieldName(), rev);
213        }
214        else
215        {
216            if(proxy.hasMethod(accessId.getWriterMethodName()))
217                proxy.invoke(accessId.getWriterMethodName(), id);
218            if(proxy.hasMethod(accessRev.getWriterMethodName()))
219                proxy.invoke(accessRev.getWriterMethodName(), rev);
220        }
221    }
222
223    
224    @Override
225    public String getBody()
226    {
227        return this.body;
228    }
229    
230    @Override
231    public HttpMethod asDelete()
232    {
233        this.method = HttpMethod.DELETE;
234        return this.method;
235    }
236}