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.Consts;
026import org.apache.http.Header;
027import org.apache.http.HeaderIterator;
028import org.apache.http.HttpEntity;
029import org.apache.http.StatusLine;
030import org.apache.http.client.methods.CloseableHttpResponse;
031import org.apache.http.client.methods.HttpPost;
032import org.apache.http.client.methods.HttpPut;
033import org.apache.http.client.methods.HttpRequestBase;
034import org.apache.http.entity.StringEntity;
035import org.apache.http.params.HttpParams;
036import org.apache.http.protocol.HTTP;
037import org.apache.http.util.EntityUtils;
038import org.slf4j.Logger;
039
040import net.sf.jkniv.exception.HandleableException;
041import net.sf.jkniv.exception.HandlerException;
042import net.sf.jkniv.reflect.beans.ObjectProxy;
043import net.sf.jkniv.reflect.beans.PropertyAccess;
044import net.sf.jkniv.sqlegance.RepositoryException;
045import net.sf.jkniv.sqlegance.dialect.SqlFeatureSupport;
046import net.sf.jkniv.whinstone.Param;
047import net.sf.jkniv.whinstone.Queryable;
048import net.sf.jkniv.whinstone.commands.Command;
049import net.sf.jkniv.whinstone.commands.CommandHandler;
050import net.sf.jkniv.whinstone.commands.NoCommandHandler;
051import net.sf.jkniv.whinstone.couchdb.statement.CouchDbStatementAdapter;
052import net.sf.jkniv.whinstone.params.ParameterNotFoundException;
053
054/**
055 * 
056 * HTTP code documentation from 
057 * <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.18">W3C RFC 2616</a>
058 * 
059 * @author Alisson Gomes
060 * @since 0.6.0
061 */
062public abstract class AbstractCommand implements CouchCommand
063{
064    private static final Logger        LOGSQL = net.sf.jkniv.whinstone.couchdb.LoggerFactory.getLogger();
065    //protected final static String COUCHDB_ID  = "id";
066    //protected final static String COUCHDB_REV = "rev";
067    protected HandleableException      handlerException;
068    protected CommandHandler           commandHandler;
069    protected String                   url;
070    protected String                   body;
071    protected HttpMethod               method;
072    CouchDbStatementAdapter<?, String> stmt;
073    
074    public AbstractCommand()
075    {
076        this(null);
077    }
078    
079    public AbstractCommand(String url)
080    {
081        this(url, null);
082    }
083    
084    public AbstractCommand(String url, String body)
085    {
086        // TODO design exception message to handler exception
087        this.handlerException = new HandlerException(RepositoryException.class, "Cannot set parameter [%s] value [%s]");
088        this.url = url;
089        this.body = body;
090        this.method = HttpMethod.GET;
091        this.commandHandler = NoCommandHandler.getInstance();
092    }
093    
094    @Override
095    public <T> Command with(T stmt)
096    {
097        this.stmt = (CouchDbStatementAdapter) stmt;
098        this.stmt.rows();
099        this.body = this.stmt.getBody();
100        return this;
101    }
102    
103    @Override
104    public Command with(HandleableException handlerException)
105    {
106        this.handlerException = handlerException;
107        return this;
108    }
109    
110    @Override
111    public Command with(CommandHandler commandHandler)
112    {
113        this.commandHandler = commandHandler;
114        return this;
115    }
116    
117    protected HttpEntity getEntity()
118    {
119        HttpEntity entity = null;
120        entity = new StringEntity(body, Consts.UTF_8); // TODO config charset for HTTP body 
121        return entity;
122    }
123    
124    protected String getContentType(HttpRequestBase http)
125    {
126        String content = "";
127        Header h = http.getFirstHeader(HTTP.CONTENT_TYPE);
128        if (h != null)
129            content = h.getValue();
130        return content;
131    }
132    
133    protected String getContentEncode(HttpRequestBase http)
134    {
135        String content = "";
136        Header h = http.getFirstHeader(HTTP.CONTENT_ENCODING);
137        if (h != null)
138            content = h.getValue();
139        return content;
140    }
141    
142    protected String errorFormat(HttpRequestBase http, StatusLine statusLine, String json)
143    {
144        StringBuilder sb = new StringBuilder("URI -> " + http.getURI());
145        try
146        {
147            if (http instanceof HttpPost)
148                sb.append("\nHttp Body\n" + EntityUtils.toString(((HttpPost) http).getEntity()));
149            else if (http instanceof HttpPut)
150                sb.append("\nHttp Body\n" + EntityUtils.toString(((HttpPut) http).getEntity()));
151        }
152        catch (IOException io)
153        {
154            sb.append("\nHttp Body fail \n ***" + io.getMessage() + "***");
155        }
156        sb.append("\n" + http.getMethod() + " " + statusLine.toString() + " " + getContentType(http) + " "
157                + getContentEncode(http) + " -> " + json);
158        
159        return sb.toString();
160    }
161    
162    @Override
163    public HttpMethod asPut()
164    {
165        throw new RepositoryException("Abstract Command cannot be executed as PUT method");
166    }
167    
168    @Override
169    public HttpMethod asPost()
170    {
171        throw new RepositoryException("Abstract Command cannot be executed as POST method");
172    }
173    
174    @Override
175    public HttpMethod asDelete()
176    {
177        throw new RepositoryException("Abstract Command cannot be executed as DELETE method");
178    }
179    
180    @Override
181    public HttpMethod asGet()
182    {
183        throw new RepositoryException("Abstract Command cannot be executed as GET method");
184    }
185    
186    @Override
187    public HttpMethod asHead()
188    {
189        throw new RepositoryException("Abstract Command cannot be executed as HEAD method");
190    }
191    
192    /**
193     * Verify if http status represents a record not found
194     * @param statusCode HTTP status code
195     * @return return {@code true} when the resource it's not found, {@code false} otherwise.
196     */
197    protected boolean isNotFound(int statusCode)
198    {
199        return (statusCode == HTTP_NO_CONTENT || statusCode == HTTP_NOT_MODIFIED || statusCode == HTTP_RESET_CONTENT
200                || statusCode == HTTP_NOT_FOUND);
201    }
202    
203    protected boolean isOk(int statusCode)
204    {
205        return (statusCode == HTTP_OK);
206    }
207    
208    /**
209     * 201 Created
210     * <p>
211     * The request has been fulfilled and resulted in a new resource being created. 
212     * The newly created resource can be referenced by the URI(s) returned in the 
213     * entity of the response, with the most specific URI for the resource given by 
214     * a Location header field. The response SHOULD include an entity containing a 
215     * list of resource characteristics and location(s) from which the user or user 
216     * agent can choose the one most appropriate. The entity format is specified by 
217     * the media type given in the Content-Type header field. The origin server MUST 
218     * create the resource before returning the 201 status code. If the action cannot 
219     * be carried out immediately, the server SHOULD respond with 202 (Accepted) 
220     * response instead. 
221     * 
222     * @param statusCode HTTP status code
223     * @return {@code true} when is 202 code, {@code false} otherwise
224     */
225    protected boolean isCreated(int statusCode)
226    {
227        return (statusCode == HTTP_CREATED);
228    }
229    
230    /**
231     * 202 Accepted
232     * <p>
233     * The request has been accepted for processing, but the processing has not 
234     * been completed. The request might or might not eventually be acted upon, 
235     * as it might be disallowed when processing actually takes place. There is 
236     * no facility for re-sending a status code from an asynchronous operation 
237     * such as this.
238     * <p>
239     * The 202 response is intentionally non-committal. Its purpose is to allow 
240     * a server to accept a request for some other process (perhaps a batch-oriented 
241     * process that is only run once per day) without requiring that the user agent's 
242     * connection to the server persist until the process is completed. The entity 
243     * returned with this response SHOULD include an indication of the request's current 
244     * status and either a pointer to a status monitor or some estimate of when the user 
245     * can expect the request to be fulfilled. 
246     *  
247     * @param statusCode HTTP status code
248     * @return {@code true} when is 202 code, {@code false} otherwise
249     */
250    protected boolean isAccepted(int statusCode)
251    {
252        return (statusCode == HTTP_ACCEPTED);
253    }
254    
255    /**
256     * 417 Expectation Faile
257     * <p>
258     * The expectation given in an Expect request-header field (see section 14.20) 
259     * could not be met by this server, or, if the server is a proxy, the server has 
260     * unambiguous evidence that the request could not be met by the next-hop server.
261     * 
262     *  @param statusCode HTTP status code
263     *  @return {@code true} when is 202 code, {@code false} otherwise
264     */
265    protected boolean isExpectationFailed(int statusCode)
266    {
267        return (statusCode == HTTP_EXPECTATION_FAILED);
268    }
269    
270    protected String getRevision(Queryable queryable)
271    {
272        PropertyAccess accessRev = queryable.getDynamicSql().getSqlDialect().getAccessRevision();
273        Param rev = getProperty(queryable, accessRev.getFieldName());
274        return rev.getValue().toString();
275    }
276    
277    @SuppressWarnings(
278    { "rawtypes", "unchecked" })
279    protected void injectIdentity(ObjectProxy<?> proxy, Object param, String id, String rev, PropertyAccess accessId,
280            PropertyAccess accessRev)
281    {
282        if (param instanceof Map)
283        {
284            Map map = (Map) param;
285            if (!map.containsKey(accessId.getFieldName()))
286                map.put(accessId.getFieldName(), id);
287            map.put(accessRev.getFieldName(), rev);
288        }
289        else
290        {
291            if (proxy.hasMethod(accessId.getWriterMethodName()))
292                proxy.invoke(accessId.getWriterMethodName(), id);
293            if (proxy.hasMethod(accessRev.getWriterMethodName()))
294                proxy.invoke(accessRev.getWriterMethodName(), rev);
295        }
296    }
297    
298    @SuppressWarnings(
299    { "rawtypes", "unchecked" })
300    protected void injectAutoIdentity(ObjectProxy<?> proxy, Object param, String id, String rev, String properName,
301            PropertyAccess accessId, PropertyAccess accessRev)
302    {
303        if (param instanceof Map)
304        {
305            Map map = (Map) param;
306            if (!map.containsKey(properName))
307                map.put(properName, id);
308            map.put(accessRev.getFieldName(), rev);
309        }
310        else
311        {
312            if (proxy.hasMethod(accessId.getWriterMethodName()))
313                proxy.invoke(accessId.getWriterMethodName(), id);
314        }
315    }
316    
317    protected void setBookmark(String bookmark, Queryable queryable)
318    {
319        if (bookmark != null
320                && queryable.getDynamicSql().getSqlDialect().supportsFeature(SqlFeatureSupport.BOOKMARK_QUERY))
321            queryable.setBookmark(bookmark);
322    }
323    
324    private Param getProperty(Queryable queryable, String name)
325    {
326        Param v = null;
327        try
328        {
329            v = queryable.getProperty(name);
330        }
331        catch (ParameterNotFoundException ignore)
332        {
333            /* parameter not exixts */}
334        return (v != null ? v : new Param());
335    }
336    
337    protected void printRequest(HttpRequestBase req)
338    {
339        if (LOGSQL.isTraceEnabled())
340        {
341            StringBuilder sb = new StringBuilder("\n ")
342                    .append(req.getProtocolVersion().toString())
343                    .append(" ").append(req.getMethod())
344                    .append(" ").append(req.getURI());
345
346            for (Header h : req.getAllHeaders())
347                sb.append("\n ").append(h.getName()+": "+h.getValue());
348            
349            if (!"GET".equalsIgnoreCase(req.getMethod()))
350                sb.append("\n").append(body);
351            
352            LOGSQL.info(sb.toString());
353        }
354    }
355    
356    protected void printResponse(CloseableHttpResponse resp, String json)
357    {
358        if (LOGSQL.isTraceEnabled())
359        {
360            StringBuilder sb = new StringBuilder("\n").append(resp.getStatusLine().toString());
361            HeaderIterator it = resp.headerIterator();
362            while (it.hasNext())
363            {
364                Header header = it.nextHeader();
365                sb.append("\n ")
366                .append(header.getName())
367                .append(": ")
368                .append(header.getValue());
369            }
370            sb.append("\n").append("Response").append("\n").append(json);
371            LOGSQL.trace(sb.toString());
372        }
373    }    
374}