View Javadoc
1   package net.technearts.rip;
2   
3   import static java.nio.charset.StandardCharsets.UTF_8;
4   import static java.util.Arrays.asList;
5   import static java.util.Optional.ofNullable;
6   import static net.technearts.rip.OP.AND;
7   import static net.technearts.rip.OP.OR;
8   import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404;
9   import static org.eclipse.jetty.http.HttpStatus.OK_200;
10  
11  import java.io.IOException;
12  import java.nio.file.Files;
13  import java.nio.file.Path;
14  import java.util.Arrays;
15  import java.util.HashMap;
16  import java.util.LinkedHashMap;
17  import java.util.Map;
18  import java.util.Optional;
19  import java.util.function.BiFunction;
20  import java.util.function.Consumer;
21  import java.util.function.Function;
22  import java.util.function.Predicate;
23  
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import net.sf.jmimemagic.Magic;
28  import net.sf.jmimemagic.MagicException;
29  import net.sf.jmimemagic.MagicMatch;
30  import net.sf.jmimemagic.MagicMatchNotFoundException;
31  import net.sf.jmimemagic.MagicParseException;
32  import spark.ModelAndView;
33  import spark.Request;
34  import spark.Response;
35  
36  enum OP {
37    AND, OR;
38  }
39  
40  /**
41   * Um construtor de respostas com base no conteúdo do body da requisição http.
42   */
43  public class RipResponseBuilder {
44    // TODO passar os três mapas para dentro do RipRoute
45    private static final Map<RipRoute, Map<Predicate<Request>, RipResponse>> CONDITIONS = new LinkedHashMap<>();
46    private static final Map<RipRoute, BiFunction<Request, Response, String>> LOGS = new LinkedHashMap<>();
47    private static final Logger LOG = LoggerFactory
48        .getLogger(RipResponseBuilder.class);
49  
50    private RipRoute route;
51    private Predicate<Request> condition;
52    private OP op = AND;
53  
54    RipResponseBuilder(final RipRoute route) {
55      LOG.info("Criando RipResponseBuilder para requisição {} {}",
56          route.getMethod(), route.getPath());
57      this.route = route;
58      if (!CONDITIONS.containsKey(route)) {
59        CONDITIONS.put(route,
60            new LinkedHashMap<Predicate<Request>, RipResponse>());
61      }
62      // TODO isso deveria estar no RipResponse
63      route.route = (req, res) -> {
64        final Optional<Map.Entry<Predicate<Request>, RipResponse>> optional = CONDITIONS
65            .get(route).entrySet().stream()
66            .filter(entry -> entry.getKey().test(req)).findFirst();
67        RipResponse response;
68        String result;
69        if (optional.isPresent()) {
70          response = optional.get().getValue();
71          LOG.debug("Requisição para {}:\n{}", req.pathInfo(), req.body());
72          LOG.debug("Respondendo com \n{}", response.getContent());
73          res.status(response.getStatus());
74          result = response.getContent();
75          if (response.getContentType() == null) {
76            res.header("Content-Type", contentType(result.getBytes(UTF_8)));
77          } else {
78            res.header("Content-Type", response.getContentType());
79          }
80        } else {
81          res.status(NOT_FOUND_404);
82          LOG.warn("Resposta para {} {} não encontrada", route.getMethod(),
83              route.getPath());
84          result = "";
85        }
86        ofNullable(LOGS.get(route)).ifPresent(f -> f.apply(req, res));
87        return result;
88      };
89      route.templateRoute = (req, res) -> {
90        final Optional<Map.Entry<Predicate<Request>, RipResponse>> optional = CONDITIONS
91            .get(route).entrySet().stream()
92            .filter(entry -> entry.getKey().test(req)).findFirst();
93        RipResponse response;
94        ModelAndView result;
95        if (optional.isPresent()) {
96          response = optional.get().getValue();
97          LOG.debug("Respondendo com \n{}", response.getContent());
98          res.status(response.getStatus());
99          final Map<String, Object> attributes = new HashMap<>();
100         for (final Map.Entry<String, Function<Request, String>> f : response
101             .getAttributes().entrySet()) {
102           attributes.put(f.getKey(), f.getValue().apply(req));
103         }
104         if (response.getContentType() != null) {
105           res.header("Content-Type", response.getContentType());
106         } else {
107           res.header("Content-Type", "text/plain");
108         }
109         result = new ModelAndView(attributes, response.getContent());
110       } else {
111         res.status(NOT_FOUND_404);
112         LOG.debug("Resposta para {} {} não encontrada", route.getMethod(),
113             route.getPath());
114         result = null;
115       }
116       return result;
117     };
118   }
119 
120   /**
121    * Operador lógico E
122    *
123    * @return this
124    */
125   public RipResponseBuilder and() {
126     op = AND;
127     return this;
128   }
129 
130   /**
131    * Cria uma resposta utilizando um arquivo de template, substituindo as
132    * variáveis no arquivo pelo resultado de cada aplicação da função.
133    *
134    * O mapa é alterado através de um <code>Consumer</code> para conveniência
135    *
136    * @param template  o arquivo de template
137    * @param consumers lista de alterações ao mapa de variáveis X funções
138    */
139   @SafeVarargs
140   public final void buildResponse(final String template,
141       final Consumer<Map<String, Function<Request, String>>>... consumers) {
142     buildResponse(template, OK_200, consumers);
143   }
144 
145   @SafeVarargs
146   public final void buildResponse(final String template, final int status,
147       final Consumer<Map<String, Function<Request, String>>>... consumers) {
148     buildResponse(template, status, null, consumers);
149   }
150 
151   /**
152    * Cria uma resposta utilizando um arquivo de template, substituindo as
153    * variáveis no arquivo pelo resultado de cada aplicação da função.
154    *
155    * O mapa é alterado através de um <code>Consumer</code> para conveniência
156    *
157    * @param template  o arquivo de template
158    * @param status    o status de retorno
159    * @param consumers lista de alterações ao mapa de variáveis X funções
160    */
161   @SafeVarargs
162   public final void buildResponse(final String template, final int status,
163       final String contentType,
164       final Consumer<Map<String, Function<Request, String>>>... consumers) {
165     final Map<String, Function<Request, String>> attributes = new HashMap<>();
166     for (final Consumer<Map<String, Function<Request, String>>> consumer : consumers) {
167       consumer.accept(attributes);
168     }
169     buildResponse(template, status, contentType, attributes);
170   }
171 
172   /**
173    * Cria uma resposta utilizando um arquivo de template, substituindo as
174    * variáveis no arquivo pelo resultado de cada aplicação da função.
175    *
176    * @param template   o arquivo de template
177    * @param status     o status de retorno
178    * @param attributes lista de alterações ao mapa de variáveis X funções
179    */
180   public final void buildResponse(final String template, final int status,
181       final String contentType,
182       final Map<String, Function<Request, String>> attributes) {
183     if (condition == null) {
184       condition = s -> true;
185     }
186     final RipResponse res = new RipResponse(attributes, template, status,
187         contentType);
188     CONDITIONS.get(route).put(condition, res);
189     route.createTemplateMethod();
190   }
191 
192   /**
193    * Cria uma resposta utilizando um arquivo de template, substituindo as
194    * variáveis no arquivo pelo resultado de cada aplicação da função.
195    *
196    * @param template   o arquivo de template
197    * @param attributes lista de alterações ao mapa de variáveis X funções
198    */
199   public final void buildResponse(final String template,
200       final Map<String, Function<Request, String>> attributes) {
201     buildResponse(template, OK_200, null, attributes);
202   }
203 
204   @SafeVarargs
205   public final void buildResponse(final String template,
206       final String contentType,
207       final Consumer<Map<String, Function<Request, String>>>... consumers) {
208     buildResponse(template, OK_200, contentType, consumers);
209   }
210 
211   public final void buildResponse(final String template,
212       final String contentType,
213       final Map<String, Function<Request, String>> attributes) {
214     buildResponse(template, OK_200, contentType, attributes);
215   }
216 
217   /**
218    * Verifica se o body da requisição http contém determinada sequência
219    *
220    * @param content o conteúdo a ser checado no body
221    * @return this
222    */
223   public RipResponseBuilder contains(final String content) {
224     final Predicate<Request> newCondition = req -> req.body().contains(content);
225     updateConditions(newCondition);
226     return this;
227   }
228 
229   /**
230    * Verifica se o body da requisição http contém todas as sequências informadas
231    *
232    * @param contents os conteúdos a serem checados no body
233    * @return this
234    */
235   public RipResponseBuilder containsAll(final String... contents) {
236     final Predicate<Request> newCondition = req -> Arrays.asList(contents)
237         .stream().allMatch(req.body()::contains);
238     updateConditions(newCondition);
239     return this;
240   }
241 
242   /**
243    * Verifica se o body da requisição http contém alguma das sequências
244    * informadas
245    *
246    * @param contents os conteúdos a serem checados no body
247    * @return this
248    */
249   public RipResponseBuilder containsAny(final String... contents) {
250     final Predicate<Request> newCondition = req -> Arrays.asList(contents)
251         .stream().anyMatch(req.body()::contains);
252     updateConditions(newCondition);
253     return this;
254   }
255 
256   private String contentType(final byte[] stream) {
257     MagicMatch match = null;
258     try {
259       match = Magic.getMagicMatch(stream, false);
260       if (match.getSubMatches().size() > 0) {
261         return match.getSubMatches().toArray()[0].toString();
262       }
263     } catch (MagicParseException | MagicMatchNotFoundException | MagicException
264         | NullPointerException e) {
265       return "text/html;charset=utf-8";
266     }
267     return match.getMimeType();
268   }
269 
270   /**
271    * Cria um log dos objetos Request/Response na chamada ao Route. Várias
272    * chamadas ao método apenas substituem o log criado anteriormente.
273    *
274    * @param f a Função que irá retornar a mensagem de log
275    * @return this
276    */
277   public RipResponseBuilder log(final BiFunction<Request, Response, String> f) {
278     LOGS.put(route, f);
279     return this;
280   }
281 
282   /**
283    * Verifica se o body da requisição http contém determinada sequência
284    *
285    * @param condition a condição a ser checada
286    * @return this
287    */
288   public RipResponseBuilder matches(final Predicate<Request> condition) {
289     updateConditions(condition);
290     return this;
291   }
292 
293   /**
294    * Verifica se o body da requisição http contém todas as sequências informadas
295    *
296    * @param conditions as condições a serem checadas
297    * @return this
298    */
299   public RipResponseBuilder matchesAll(
300       @SuppressWarnings("unchecked") final Predicate<Request>... conditions) {
301     final Predicate<Request> newCondition = asList(conditions).stream()
302         .reduce(req -> true, Predicate::and);
303     updateConditions(newCondition);
304     return this;
305   }
306 
307   /**
308    * Verifica se o body da requisição http contém alguma das sequências
309    * informadas
310    *
311    * @param conditions as condições a serem checadas
312    * @return this
313    */
314   public RipResponseBuilder matchesAny(
315       @SuppressWarnings("unchecked") final Predicate<Request>... conditions) {
316     final Predicate<Request> newCondition = asList(conditions).stream()
317         .reduce(req -> false, Predicate::or);
318     updateConditions(newCondition);
319     return this;
320   }
321 
322   /**
323    * Operador lógico OU
324    *
325    * @return this
326    */
327   public RipResponseBuilder or() {
328     op = OR;
329     return this;
330   }
331 
332   /**
333    * Cria uma resposta com o conteúdo do arquivo informado. Essa é uma operação
334    * terminal.
335    *
336    * @param withFile o caminho relativo para o arquivo, com raiz em
337    *                 src/main/resources
338    */
339   public void respond(final Path withFile) {
340     respond(withFile, OK_200);
341   }
342 
343   /**
344    * Cria uma resposta com o conteúdo do arquivo informado, retornando o
345    * <code>status</code> http. Essa é uma operação terminal.
346    *
347    * @param withFile o caminho relativo para o arquivo, com raiz em
348    *                 src/main/resources
349    * @param status   o status de retorno
350    */
351   public void respond(final Path withFile, final int status) {
352     respond(withFile, status, null);
353   }
354 
355   public void respond(final Path withFile, final int status,
356       final String contentType) {
357     try {
358       respond(new String(Files.readAllBytes(withFile)), status, contentType);
359     } catch (final IOException e) {
360       respond("Arquivo não encontrado.", NOT_FOUND_404);
361     }
362   }
363 
364   public void respond(final Path withFile, final String contentType) {
365     respond(withFile, OK_200, contentType);
366   }
367 
368   /**
369    * Cria uma resposta com o conteúdo do arquivo informado, retornando o
370    * <code>status</code> http. Essa é uma operação terminal.
371    *
372    * @param response o conteúdo do corpo da mensagem de retorno
373    */
374   public void respond(final String response) {
375     respond(response, OK_200);
376   }
377 
378   /**
379    * Cria uma resposta com o conteúdo do arquivo informado, retornando o
380    * <code>status</code> http. Essa é uma operação terminal.
381    *
382    * @param response o conteúdo do corpo da mensagem de retorno
383    * @param status   o status de retorno
384    */
385   public void respond(final String response, final int status) {
386     respond(response, status, null);
387   }
388 
389   public void respond(final String response, final int status,
390       final String contentType) {
391     if (condition == null) {
392       condition = s -> true;
393     }
394     final RipResponse res = new RipResponse(response, status, contentType);
395     CONDITIONS.get(route).put(condition, res);
396     route.createMethod();
397   }
398 
399   public void respond(final String response, final String contentType) {
400     respond(response, OK_200, contentType);
401   }
402 
403   private void updateConditions(final Predicate<Request> newCondition) {
404     if (condition == null) {
405       condition = newCondition;
406     } else {
407       switch (op) {
408       case OR:
409         condition = condition.or(newCondition);
410         break;
411       case AND:
412         condition = condition.and(newCondition);
413         break;
414       }
415     }
416   }
417 
418 }