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}