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}