001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.petstore.console.dev;
018
019import static java.nio.charset.StandardCharsets.*;
020
021import java.io.*;
022import java.util.*;
023
024import org.apache.juneau.http.*;
025import org.apache.juneau.http.response.*;
026import org.apache.juneau.marshall.collections.*;
027import org.apache.juneau.petstore.console.*;
028import org.apache.juneau.petstore.console.pages.*;
029import org.apache.juneau.petstore.dto.*;
030import org.apache.juneau.rest.server.*;
031import org.apache.juneau.rest.server.view.*;
032import org.apache.juneau.rest.server.view.freemarker.*;
033
034/**
035 * The Custom cards page, which shows how an adopter writes a card type, and the feeds its cards read.
036 *
037 * <p>
038 * The page has a JSON-only card type ({@code kpi}), a Java-validated one ({@code inventory-gauge}, see
039 * {@link InventoryGaugeCardType}) and two charts: the stock bands and the zoomable prices.
040 *
041 * <p>
042 * Both JS handlers live in {@code petstore-cards.js}, served beside this class, which also shows the card API
043 * ({@code JuneauConsole.refreshCard}, {@code JuneauConsole.cardApi}) and the {@code juneau:card-mounted} event.
044 *
045 * <h5 class='section'>Example:</h5>
046 * <p class='bjs'>
047 *    (window.JuneauConsoleCards = window.JuneauConsoleCards || []).push([<js>'kpi'</js>, {
048 *       render(card, el, ctx) { ... }
049 *    }]);
050 * </p>
051 * <p class='bcode'>
052 *    GET /console/dev/cards                       the page
053 *    GET /console/dev/cards/stats/available       {label, value} for the kpi card
054 *    GET /console/dev/cards/inventory?species=    {species, available, pending, sold} for the gauge
055 *    GET /console/dev/cards/inventory/all         one gauge row per species, for the stock bands chart
056 *    GET /console/dev/cards/prices                {id, price} per pet, for the prices chart
057 *    GET /console/dev/cards/petstore-cards.js     the page script
058 * </p>
059 */
060@Rest(path="/cards", title="Custom cards")
061@SuppressWarnings({
062   "java:S110", // Inheritance depth comes from the BasicRestServlet hierarchy, not this page.
063   "resource" // The response writer is owned by the servlet container; Eclipse JDT @Owning warning is by design.
064})
065public class CustomCardsRest extends PetstoreConsolePage {
066
067   private static final long serialVersionUID = 1L;
068
069   /**
070    * The Custom cards page.
071    *
072    * @param req The request; {@code ?builder} renders the builder twin.
073    * @return The page.
074    */
075   @RestGet(path="/")
076   public View page(RestRequest req) {
077      return builder(req) ? twin(req, DevPageBuilders.cards()) : FreemarkerView.of("cards.ftlh");
078   }
079
080   /** @return The kpi card's number: how many pets are available. */
081   @RestGet(path="/stats/available")
082   public JsonMap available() {
083      var n = store().getPets().stream().filter(p -> p.getStatus() == PetStatus.AVAILABLE).count();
084      return new JsonMap().append("label", "Pets available").append("value", n);
085   }
086
087   /**
088    * Counts one species' pets by status, for the gauge.
089    *
090    * @param species The species name, case-insensitive.
091    * @return {@code {species, available, pending, sold}}.
092    * @throws BadRequest If the species is missing or unknown.
093    */
094   @RestGet(path="/inventory")
095   public JsonMap inventory(@Query("species") String species) {
096      if (species == null)
097         throw new BadRequest("Missing 'species'");
098      Species s;
099      try {
100         s = Species.valueOf(species.toUpperCase(Locale.ROOT));
101      } catch (IllegalArgumentException e) {
102         throw new BadRequest(e, "Unknown species '%s'", species);
103      }
104      var counts = new EnumMap<PetStatus,Long>(PetStatus.class);
105      for (var st : PetStatus.values())
106         counts.put(st, 0L);
107      store().getPets().stream().filter(p -> p.getSpecies() == s).forEach(p -> counts.merge(p.getStatus(), 1L, Long::sum));
108      return new JsonMap().append("species", s.name())
109         .append("available", counts.get(PetStatus.AVAILABLE))
110         .append("pending", counts.get(PetStatus.PENDING))
111         .append("sold", counts.get(PetStatus.SOLD));
112   }
113
114   /** @return One {@link #inventory(String)} row per species, in enum order, for the stock bands chart. */
115   @RestGet(path="/inventory/all")
116   public JsonList inventoryAll() {
117      var rows = new JsonList();
118      Arrays.stream(Species.values()).forEach(s -> rows.add(inventory(s.name())));
119      return rows;
120   }
121
122   /** @return One row {@code {id, price}} per pet, by id, for the zoomable prices chart. */
123   @RestGet(path="/prices")
124   public JsonList prices() {
125      var rows = new JsonList();
126      store().getPets().stream().sorted(Comparator.comparingLong(Pet::getId))
127         .forEach(p -> rows.add(new JsonMap().append("id", p.getId()).append("price", p.getPrice())));
128      return rows;
129   }
130
131   /**
132    * Serves the page-local script.
133    *
134    * @param res The response.
135    * @throws IOException If the bundled script can't be read.
136    */
137   @RestGet(path="/petstore-cards.js")
138   public void script(RestResponse res) throws IOException {
139      try (var in = CustomCardsRest.class.getResourceAsStream("petstore-cards.js")) {
140         if (in == null)
141            throw new IllegalStateException("Missing classpath resource 'petstore-cards.js' beside " + CustomCardsRest.class.getName());
142         res.setContentType("text/javascript;charset=utf-8");
143         res.getWriter().write(new String(in.readAllBytes(), UTF_8));
144      }
145   }
146}