View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.gateway.kernel.rest;
21  
22  import com.fasterxml.jackson.core.JsonProcessingException;
23  import com.fasterxml.jackson.databind.node.ObjectNode;
24  import io.netty.buffer.ByteBuf;
25  import io.netty.buffer.Unpooled;
26  import io.netty.channel.ChannelFuture;
27  import io.netty.channel.ChannelHandlerContext;
28  import io.netty.handler.codec.http.HttpHeaderNames;
29  import io.netty.handler.codec.http.HttpResponse;
30  import io.netty.handler.codec.http.HttpResponseStatus;
31  import io.netty.handler.codec.http.multipart.FileUpload;
32  import org.waarp.common.database.DbPreparedStatement;
33  import org.waarp.common.database.data.AbstractDbData;
34  import org.waarp.common.database.data.AbstractDbData.UpdatedInfo;
35  import org.waarp.common.database.exception.WaarpDatabaseException;
36  import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException;
37  import org.waarp.common.database.exception.WaarpDatabaseSqlException;
38  import org.waarp.common.json.JsonHandler;
39  import org.waarp.common.logging.SysErrLogger;
40  import org.waarp.common.logging.WaarpLogger;
41  import org.waarp.common.logging.WaarpLoggerFactory;
42  import org.waarp.common.utility.WaarpStringUtils;
43  import org.waarp.gateway.kernel.exception.HttpForbiddenRequestException;
44  import org.waarp.gateway.kernel.exception.HttpIncorrectRequestException;
45  import org.waarp.gateway.kernel.exception.HttpInvalidAuthenticationException;
46  import org.waarp.gateway.kernel.exception.HttpNotFoundRequestException;
47  import org.waarp.gateway.kernel.rest.HttpRestHandler.METHOD;
48  
49  import java.nio.charset.UnsupportedCharsetException;
50  
51  /**
52   * Generic Rest Model handler for Data model (CRUD access to a database table)
53   */
54  public abstract class DataModelRestMethodHandler<E extends AbstractDbData>
55      extends RestMethodHandler {
56  
57    public enum COMMAND_TYPE {
58      MULTIGET, GET, UPDATE, CREATE, DELETE, OPTIONS
59    }
60  
61    /**
62     * Internal Logger
63     */
64    private static final WaarpLogger logger =
65        WaarpLoggerFactory.getLogger(DataModelRestMethodHandler.class);
66  
67    protected DataModelRestMethodHandler(final String name,
68                                         final RestConfiguration config,
69                                         final METHOD... method) {
70      super(name, name, true, config, METHOD.OPTIONS);
71      setMethods(method);
72    }
73  
74    protected abstract void checkAuthorization(HttpRestHandler handler,
75                                               RestArgument arguments,
76                                               RestArgument result, METHOD method)
77        throws HttpForbiddenRequestException;
78  
79    /**
80     * allowed: GET iff name or name/id, PUT iff name/id, POST iff name (no id),
81     * DELETE iff name/id and allowed
82     */
83    @Override
84    public final void checkHandlerSessionCorrectness(
85        final HttpRestHandler handler, final RestArgument arguments,
86        final RestArgument result) throws HttpForbiddenRequestException {
87      final METHOD method = arguments.getMethod();
88      if (!isMethodIncluded(method)) {
89        logger.warn("NotAllowed: " + method + ':' + arguments.getUri() + ':' +
90                    arguments.getUriArgs());
91        throw new HttpForbiddenRequestException("Unallowed Method: " + method);
92      }
93      checkAuthorization(handler, arguments, result, method);
94      final boolean hasOneExtraPathAsId = arguments.getSubUriSize() == 1;
95      final boolean hasNoExtraPath = arguments.getSubUriSize() == 0;
96      if (hasOneExtraPathAsId) {
97        arguments.addIdToUriArgs();
98      }
99      switch (method) {
100       case DELETE:
101       case PUT:
102         if (hasOneExtraPathAsId) {
103           return;
104         }
105         break;
106       case GET:
107       case OPTIONS:
108         return;
109       case POST:
110         if (hasNoExtraPath) {
111           return;
112         }
113         break;
114       default:
115         break;
116     }
117     logger.warn("NotAllowed: " + method + ':' + hasNoExtraPath + ':' +
118                 hasOneExtraPathAsId + ':' + arguments.getUri() + ':' +
119                 arguments.getUriArgs());
120     throw new HttpForbiddenRequestException(
121         "Unallowed Method and arguments combinaison");
122   }
123 
124   @Override
125   public final void getFileUpload(final HttpRestHandler handler,
126                                   final FileUpload data,
127                                   final RestArgument arguments,
128                                   final RestArgument result)
129       throws HttpIncorrectRequestException {
130     throw new HttpIncorrectRequestException("File Upload not allowed");
131   }
132 
133   @Override
134   public final Object getBody(final HttpRestHandler handler, final ByteBuf body,
135                               final RestArgument arguments,
136                               final RestArgument result)
137       throws HttpIncorrectRequestException {
138     // get the Json equivalent of the Body
139     ObjectNode node = null;
140     try {
141       final String json = body.toString(WaarpStringUtils.UTF8);
142       node = JsonHandler.getFromStringExc(json);
143     } catch (final UnsupportedCharsetException e) {
144       logger.warn("Error" + " : {}", e.getMessage());
145       throw new HttpIncorrectRequestException(e);
146     } catch (final JsonProcessingException e) {
147       result.setDetail("ERROR: JSON body cannot be parsed");
148     }
149     if (node != null) {
150       arguments.getBody().setAll(node);
151     }
152     return node;
153   }
154 
155   @Override
156   public final void endParsingRequest(final HttpRestHandler handler,
157                                       final RestArgument arguments,
158                                       final RestArgument result,
159                                       final Object body)
160       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
161              HttpNotFoundRequestException {
162     final METHOD method = arguments.getMethod();
163     switch (method) {
164       case DELETE:
165         delete(handler, arguments, result, body);
166         return;
167       case GET:
168         final boolean hasNoExtraPath = arguments.getSubUriSize() == 0;
169         if (hasNoExtraPath) {
170           getAll(handler, arguments, result, body);
171         } else {
172           getOne(handler, arguments, result, body);
173         }
174         return;
175       case OPTIONS:
176         optionsCommand(handler, arguments, result);
177         return;
178       case POST:
179         post(handler, arguments, result, body);
180         return;
181       case PUT:
182         put(handler, arguments, result, body);
183         return;
184       default:
185         break;
186     }
187     throw new HttpIncorrectRequestException("Incorrect request: " + method);
188   }
189 
190   /**
191    * For Read or Update, should include a select() from the database. Shall
192    * not
193    * be used for Create. JSON_ID
194    * should be checked for the primary id.
195    *
196    * @param handler
197    * @param arguments
198    * @param result
199    * @param body
200    *
201    * @return the Object E according to URI and other arguments
202    *
203    * @throws HttpIncorrectRequestException
204    * @throws HttpInvalidAuthenticationException
205    * @throws HttpNotFoundRequestException
206    */
207   protected abstract E getItem(HttpRestHandler handler, RestArgument arguments,
208                                RestArgument result, Object body)
209       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
210              HttpNotFoundRequestException;
211 
212   /**
213    * To be used only in create mode. No insert should be done into the
214    * database.
215    *
216    * @param handler
217    * @param arguments
218    * @param result
219    * @param body
220    *
221    * @return a new Object E according to URI and other arguments
222    *
223    * @throws HttpIncorrectRequestException
224    * @throws HttpInvalidAuthenticationException
225    */
226   protected abstract E createItem(HttpRestHandler handler,
227                                   RestArgument arguments, RestArgument result,
228                                   Object body)
229       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException;
230 
231   /**
232    * For getAll access
233    *
234    * @param handler
235    * @param arguments
236    * @param result
237    * @param body
238    *
239    * @return the associated preparedStatement
240    *
241    * @throws HttpIncorrectRequestException
242    * @throws HttpInvalidAuthenticationException
243    */
244   protected abstract DbPreparedStatement getPreparedStatement(
245       HttpRestHandler handler, RestArgument arguments, RestArgument result,
246       Object body)
247       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException;
248 
249   /**
250    * @param statement
251    *
252    * @return the Object E according to statement (using next) or null if no
253    *     more
254    *     item
255    *
256    * @throws HttpIncorrectRequestException
257    * @throws HttpNotFoundRequestException
258    */
259   protected abstract E getItemPreparedStatement(DbPreparedStatement statement)
260       throws HttpIncorrectRequestException, HttpNotFoundRequestException;
261 
262   /**
263    * @return the primary property name used in the uri for Get,Put,Delete for
264    *     unique access
265    */
266   public abstract String getPrimaryPropertyName();
267 
268   protected final void setOk(final HttpRestHandler handler,
269                              final RestArgument result) {
270     handler.setStatus(HttpResponseStatus.OK);
271     result.setResult(HttpResponseStatus.OK);
272   }
273 
274   /**
275    * Get all items, according to a possible filter
276    *
277    * @param handler
278    * @param arguments
279    * @param result
280    * @param body
281    *
282    * @throws HttpIncorrectRequestException
283    * @throws HttpInvalidAuthenticationException
284    * @throws HttpNotFoundRequestException
285    */
286   protected final void getAll(final HttpRestHandler handler,
287                               final RestArgument arguments,
288                               final RestArgument result, final Object body)
289       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
290              HttpNotFoundRequestException {
291     final long limit = arguments.getLimitFromUri();
292     final DbPreparedStatement statement =
293         getPreparedStatement(handler, arguments, result, body);
294     try {
295       result.addFilter((ObjectNode) body);
296       int count = 0;
297       try {
298         statement.executeQuery();
299       } catch (final WaarpDatabaseNoConnectionException e) {
300         throw new HttpIncorrectRequestException(e);
301       } catch (final WaarpDatabaseSqlException e) {
302         throw new HttpNotFoundRequestException(e);
303       }
304       try {
305         for (; count < limit && statement.getNext(); count++) {
306           final E item = getItemPreparedStatement(statement);
307           if (item != null) {
308             result.addResult(item.getJson());
309           }
310         }
311       } catch (final WaarpDatabaseNoConnectionException e) {
312         throw new HttpIncorrectRequestException(e);
313       } catch (final WaarpDatabaseSqlException e) {
314         throw new HttpNotFoundRequestException(e);
315       }
316       result.addCountLimit(count, limit);
317       result.setCommand(COMMAND_TYPE.MULTIGET);
318       setOk(handler, result);
319     } finally {
320       statement.realClose();
321     }
322   }
323 
324   /**
325    * Get one item according to id
326    *
327    * @param handler
328    * @param arguments
329    * @param result
330    * @param body
331    *
332    * @throws HttpIncorrectRequestException
333    * @throws HttpInvalidAuthenticationException
334    * @throws HttpNotFoundRequestException
335    */
336   protected final void getOne(final HttpRestHandler handler,
337                               final RestArgument arguments,
338                               final RestArgument result, final Object body)
339       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
340              HttpNotFoundRequestException {
341     final E item = getItem(handler, arguments, result, body);
342     result.addAnswer(item.getJson());
343     result.setCommand(COMMAND_TYPE.GET);
344     setOk(handler, result);
345   }
346 
347   /**
348    * Update one item according to id
349    *
350    * @param handler
351    * @param arguments
352    * @param result
353    * @param body
354    *
355    * @throws HttpIncorrectRequestException
356    * @throws HttpInvalidAuthenticationException
357    * @throws HttpNotFoundRequestException
358    */
359   protected void put(final HttpRestHandler handler,
360                      final RestArgument arguments, final RestArgument result,
361                      final Object body)
362       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
363              HttpNotFoundRequestException {
364     final E item = getItem(handler, arguments, result, body);
365     try {
366       item.setFromJson(arguments.getBody(), true);
367     } catch (final WaarpDatabaseSqlException e) {
368       throw new HttpIncorrectRequestException(
369           "Issue while using Json formatting", e);
370     }
371     item.changeUpdatedInfo(UpdatedInfo.TOSUBMIT);
372     try {
373       item.update();
374     } catch (final WaarpDatabaseException e) {
375       throw new HttpIncorrectRequestException(
376           "Issue while updating to database", e);
377     }
378     result.addAnswer(item.getJson());
379     result.setCommand(COMMAND_TYPE.UPDATE);
380     setOk(handler, result);
381   }
382 
383   /**
384    * Create one item
385    *
386    * @param handler
387    * @param arguments
388    * @param result
389    * @param body
390    *
391    * @throws HttpIncorrectRequestException
392    * @throws HttpInvalidAuthenticationException
393    */
394   protected final void post(final HttpRestHandler handler,
395                             final RestArgument arguments,
396                             final RestArgument result, final Object body)
397       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException {
398     final E item = createItem(handler, arguments, result, body);
399     item.changeUpdatedInfo(UpdatedInfo.TOSUBMIT);
400     try {
401       item.insert();
402     } catch (final WaarpDatabaseException e) {
403       // Revert to update
404       try {
405         item.update();
406       } catch (final WaarpDatabaseException ex) {
407         throw new HttpIncorrectRequestException(
408             "Issue while inserting to database", e);
409       }
410     }
411     if (logger.isDebugEnabled()) {
412       logger.debug("Save {}", item.getJson());
413     }
414     result.addAnswer(item.getJson());
415     result.setCommand(COMMAND_TYPE.CREATE);
416     setOk(handler, result);
417   }
418 
419   /**
420    * delete one item
421    *
422    * @param handler
423    * @param arguments
424    * @param result
425    * @param body
426    *
427    * @throws HttpIncorrectRequestException
428    * @throws HttpInvalidAuthenticationException
429    * @throws HttpNotFoundRequestException
430    */
431   protected final void delete(final HttpRestHandler handler,
432                               final RestArgument arguments,
433                               final RestArgument result, final Object body)
434       throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
435              HttpNotFoundRequestException {
436     final E item = getItem(handler, arguments, result, body);
437     try {
438       item.delete();
439     } catch (final WaarpDatabaseException e) {
440       throw new HttpIncorrectRequestException(
441           "Issue while deleting from database", e);
442     }
443     result.addAnswer(item.getJson());
444     result.setCommand(COMMAND_TYPE.DELETE);
445     setOk(handler, result);
446   }
447 
448   @Override
449   public final ChannelFuture sendResponse(final HttpRestHandler handler,
450                                           final ChannelHandlerContext ctx,
451                                           final RestArgument arguments,
452                                           final RestArgument result,
453                                           final Object body,
454                                           final HttpResponseStatus status) {
455     final String answer = result.toString();
456     final ByteBuf buffer =
457         Unpooled.wrappedBuffer(answer.getBytes(WaarpStringUtils.UTF8));
458     final HttpResponse response = handler.getResponse(buffer);
459     if (status == HttpResponseStatus.UNAUTHORIZED) {
460       return ctx.writeAndFlush(response);
461     }
462     response.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
463     response.headers().add(HttpHeaderNames.REFERER, handler.getRequest().uri());
464     logger.debug("Will write: {}", body);
465     final ChannelFuture future = ctx.writeAndFlush(response);
466     if (handler.isWillClose()) {
467       SysErrLogger.FAKE_LOGGER.syserr(
468           "Will close session in DataModelRestMethodHandler");
469       return future;
470     }
471     return null;
472   }
473 }