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.service; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.nio.charset.*; 023import java.time.*; 024import java.util.*; 025import java.util.concurrent.*; 026import java.util.concurrent.atomic.*; 027import java.util.function.*; 028 029import org.apache.juneau.commons.beanquery.*; 030import org.apache.juneau.marshall.marshaller.*; 031import org.apache.juneau.petstore.dto.*; 032 033/** 034 * In-memory {@link Pet}/{@link Order}/{@link User} store seeded from classpath JSON. 035 * 036 * <p> 037 * Backed by {@link ConcurrentHashMap}s; pet and order IDs auto-assign via {@link AtomicLong} starting from 038 * the highest seeded ID plus one (or {@code 1} if no seeds existed). Seed files live on the classpath at 039 * {@code petstore/init/{Pets,Orders,Users}.json} and are loaded lazily on first access. 040 * 041 * <p> 042 * This is a sample/demo store — there is no persistence, no transactions, and no replication. Restart wipes 043 * state back to the seeded baseline (and clears the audit trail). 044 * 045 * <p> 046 * Every successful create, update or delete is recorded in the audit trail; see {@link #getAudit()}. 047 * 048 * <h5 class='section'>See Also:</h5><ul> 049 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauPetstore">juneau-petstore</a> 050 * </ul> 051 */ 052@SuppressWarnings({ 053 "resource" // Returns an owned session; the caller closes it. 054}) 055public class PetStore { 056 057 private static final String ACTION_CREATE = "CREATE"; 058 private static final String ACTION_UPDATE = "UPDATE"; 059 private static final String ACTION_DELETE = "DELETE"; 060 private static final String ENTITY_ORDER = "Order"; 061 private static final String SEED_PETS = "petstore/init/Pets.json"; 062 private static final String SEED_ORDERS = "petstore/init/Orders.json"; 063 private static final String SEED_USERS = "petstore/init/Users.json"; 064 065 /** Actor recorded for mutations made through the {@code /petstore} API. */ 066 public static final String ACTOR_API = "api"; 067 068 /** The most pets {@link #restock} adds for one species in one call. */ 069 public static final int MAX_RESTOCK_PER_SPECIES = 1000; 070 071 /** Query context over pets; {@code tags} and {@code photo} are not searchable. */ 072 public static final InMemoryBeanQueryContext<Pet> PET_QUERY = InMemoryBeanQueryContext.create(Pet.class).exclude("tags", "photo").build(); 073 074 /** Query context over orders. */ 075 public static final InMemoryBeanQueryContext<Order> ORDER_QUERY = InMemoryBeanQueryContext.create(Order.class).build(); 076 077 /** Query context over users; {@code password} is write-only on {@link User}, so it is neither searchable nor a column. */ 078 public static final InMemoryBeanQueryContext<User> USER_QUERY = InMemoryBeanQueryContext.create(User.class).build(); 079 080 /** Query context over the audit trail. */ 081 public static final InMemoryBeanQueryContext<AuditEntry> AUDIT_QUERY = InMemoryBeanQueryContext.create(AuditEntry.class).build(); 082 083 /** Query context over staged changes. */ 084 public static final InMemoryBeanQueryContext<PendingChange> CHANGE_QUERY = InMemoryBeanQueryContext.create(PendingChange.class).build(); 085 086 private final Map<Long,Pet> pets = new ConcurrentHashMap<>(); 087 private final Map<Long,Order> orders = new ConcurrentHashMap<>(); 088 private final Map<String,User> users = new ConcurrentHashMap<>(); 089 090 private final AtomicLong nextPetId = new AtomicLong(); 091 private final AtomicLong nextOrderId = new AtomicLong(); 092 093 private final Clock clock; 094 private final List<AuditEntry> audit = new CopyOnWriteArrayList<>(); 095 private final AtomicLong nextAuditId = new AtomicLong(); 096 private final PendingChanges pendingChanges = new PendingChanges(this); 097 098 /** 099 * Constructor using the system UTC clock. 100 * 101 * <p> 102 * Eagerly loads the bundled classpath seed data. Seeding writes no audit. 103 */ 104 public PetStore() { 105 this(Clock.systemUTC()); 106 } 107 108 /** 109 * Constructor. 110 * 111 * @param clock The clock used to timestamp audit entries. Must not be <jk>null</jk>. 112 */ 113 public PetStore(Clock clock) { 114 this.clock = Objects.requireNonNull(clock, "clock"); 115 var seededPets = loadList(SEED_PETS, Pet.class); 116 var maxPetId = 0L; 117 for (var p : seededPets) { 118 if (p.getId() == 0L) 119 p.setId(++maxPetId); 120 else 121 maxPetId = Math.max(maxPetId, p.getId()); 122 pets.put(p.getId(), p); 123 } 124 nextPetId.set(maxPetId); 125 126 var seededOrders = loadList(SEED_ORDERS, Order.class); 127 var maxOrderId = 0L; 128 for (var o : seededOrders) { 129 if (o.getId() == 0L) 130 o.setId(++maxOrderId); 131 else 132 maxOrderId = Math.max(maxOrderId, o.getId()); 133 orders.put(o.getId(), o); 134 } 135 nextOrderId.set(maxOrderId); 136 137 var seededUsers = loadList(SEED_USERS, User.class); 138 for (var u : seededUsers) 139 users.put(u.getUsername(), u); 140 } 141 142 /** @return The clock this store timestamps with. */ 143 public Clock clock() { return clock; } 144 145 private static <T> List<T> loadList(String resourcePath, Class<T> elementType) { 146 var loader = PetStore.class.getClassLoader(); 147 try (var in = loader.getResourceAsStream(resourcePath)) { 148 if (in == null) 149 return l(); 150 try (var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { 151 return Json5.DEFAULT.read(reader, List.class, elementType); 152 } 153 } catch (Exception e) { 154 throw brex(e, "Failed to load petstore seed resource '%s'", resourcePath); 155 } 156 } 157 158 //------------------------------------------------------------------------------------------------------------------ 159 // Pets 160 //------------------------------------------------------------------------------------------------------------------ 161 162 /** 163 * Returns all pets. 164 * 165 * @return All pets, in arbitrary order. Never <jk>null</jk>. 166 */ 167 public Collection<Pet> getPets() { 168 // Return an immutable snapshot so callers can't mutate the backing store through the returned collection. 169 return List.copyOf(pets.values()); 170 } 171 172 /** 173 * Returns a pet by ID. 174 * 175 * @param id The pet ID. 176 * @return The pet, or <jk>null</jk> if not found. 177 */ 178 public Pet getPet(long id) { 179 return pets.get(id); 180 } 181 182 /** 183 * Creates a new pet. 184 * 185 * <p> 186 * Assigns the next available ID, ignoring any caller-supplied ID. 187 * 188 * @param pet The pet to create. Must not be <jk>null</jk>. 189 * @return The created pet (same instance, with assigned ID). 190 */ 191 public Pet createPet(Pet pet) { 192 return createPet(pet, ACTOR_API); 193 } 194 195 /** 196 * Creates a pet and records a {@code CREATE} audit entry. 197 * 198 * @param pet The pet. Its ID is overwritten. Must not be <jk>null</jk>. 199 * @param actor Who is creating it, e.g. {@code "console:alice"}. 200 * @return The stored pet. 201 */ 202 public Pet createPet(Pet pet, String actor) { 203 return addPet(pet, actor, null); 204 } 205 206 private Pet addPet(Pet pet, String actor, LongFunction<String> nameFromId) { 207 if (pet == null) 208 throw iaex("Pet must not be null"); 209 var id = nextPetId.incrementAndGet(); 210 pet.setId(id); 211 if (nameFromId != null) 212 pet.setName(nameFromId.apply(id)); 213 pets.put(id, pet); 214 recordAudit(actor, "Pet", String.valueOf(id), ACTION_CREATE, pet.getName()); 215 return pet; 216 } 217 218 /** 219 * Adds a batch of available pets, one per requested count, and records one summary {@code RESTOCK} audit entry. 220 * 221 * <h5 class='section'>Example:</h5> 222 * <p class='bjava'> 223 * <jk>var</jk> <jv>added</jv> = <jv>store</jv>.restock(Map.<jsm>of</jsm>(Species.<jsf>DOG</jsf>, 2, Species.<jsf>CAT</jsf>, 1), <js>"job:restock"</js>); 224 * <jc>// 3 new pets named "DOG #101", "DOG #102", "CAT #103" (species + store-unique id), plus one RESTOCK audit entry.</jc> 225 * </p> 226 * 227 * @param perSpecies How many pets of each species to add. Must not be <jk>null</jk>; each count must be 228 * {@code 0..}{@link #MAX_RESTOCK_PER_SPECIES}. 229 * @param actor Who is restocking, e.g. {@code "job:restock:console:alice"}. 230 * @return The created pets. 231 * @throws IllegalArgumentException If the map, a species, or a count is null, negative, or above the cap. Nothing is added. 232 */ 233 public List<Pet> restock(Map<Species,Integer> perSpecies, String actor) { 234 if (perSpecies == null) 235 throw iaex("PerSpecies must not be null"); 236 perSpecies.forEach((species, count) -> { 237 if (species == null) 238 throw iaex("Species must not be null"); 239 if (count == null || count < 0 || count > MAX_RESTOCK_PER_SPECIES) 240 throw iaex("Count for '%s' must be between 0 and %s", species, MAX_RESTOCK_PER_SPECIES); 241 }); 242 var created = new ArrayList<Pet>(); 243 perSpecies.forEach((species, count) -> { 244 for (var i = 0; i < count; i++) 245 created.add(addPet(new Pet().setSpecies(species).setPrice(9.99f).setStatus(PetStatus.AVAILABLE), actor, id -> species + " #" + id)); 246 }); 247 recordAudit(actor, "Job", "restock", "RESTOCK", created.size() + " pets"); 248 return created; 249 } 250 251 /** 252 * Updates an existing pet. 253 * 254 * @param pet The pet to update. Must not be <jk>null</jk>; must carry an ID matching an existing pet. 255 * @return The updated pet. 256 * @throws PetstoreNotFoundException If no pet with the given ID exists. 257 */ 258 public Pet updatePet(Pet pet) { 259 return updatePet(pet, ACTOR_API); 260 } 261 262 /** 263 * Replaces a pet and records an {@code UPDATE} audit entry. 264 * 265 * @param pet The pet. Must not be <jk>null</jk>. 266 * @param actor Who is updating it. 267 * @return The stored pet. 268 * @throws PetstoreNotFoundException If no pet with the given ID exists. 269 */ 270 public Pet updatePet(Pet pet, String actor) { 271 if (pet == null) 272 throw iaex("Pet must not be null"); 273 if (! pets.containsKey(pet.getId())) 274 throw new PetstoreNotFoundException("Pet not found: id=" + pet.getId()); 275 pets.put(pet.getId(), pet); 276 recordAudit(actor, "Pet", String.valueOf(pet.getId()), ACTION_UPDATE, pet.getName()); 277 return pet; 278 } 279 280 /** 281 * Deletes a pet by ID. 282 * 283 * @param id The pet ID. 284 * @throws PetstoreNotFoundException If no pet with the given ID exists. 285 */ 286 public void deletePet(long id) { 287 deletePet(id, ACTOR_API); 288 } 289 290 /** 291 * Deletes a pet and records a {@code DELETE} audit entry. 292 * 293 * @param id The pet ID. 294 * @param actor Who is deleting it. 295 * @throws PetstoreNotFoundException If no pet with the given ID exists. 296 */ 297 public void deletePet(long id, String actor) { 298 var removed = pets.remove(id); 299 if (removed == null) 300 throw new PetstoreNotFoundException("Pet not found: id=" + id); 301 recordAudit(actor, "Pet", String.valueOf(id), ACTION_DELETE, removed.getName()); 302 } 303 304 //------------------------------------------------------------------------------------------------------------------ 305 // Orders 306 //------------------------------------------------------------------------------------------------------------------ 307 308 /** 309 * Returns all orders. 310 * 311 * @return All orders, in arbitrary order. Never <jk>null</jk>. 312 */ 313 public Collection<Order> getOrders() { 314 return List.copyOf(orders.values()); 315 } 316 317 /** 318 * Returns an order by ID. 319 * 320 * @param id The order ID. 321 * @return The order, or <jk>null</jk> if not found. 322 */ 323 public Order getOrder(long id) { 324 return orders.get(id); 325 } 326 327 /** 328 * Creates a new order. 329 * 330 * <p> 331 * Assigns the next available ID, ignoring any caller-supplied ID. 332 * 333 * @param order The order to create. Must not be <jk>null</jk>. 334 * @return The created order (same instance, with assigned ID). 335 */ 336 public Order createOrder(Order order) { 337 return createOrder(order, ACTOR_API); 338 } 339 340 /** 341 * Creates an order and records a {@code CREATE} audit entry. 342 * 343 * @param order The order. Its ID is overwritten. Must not be <jk>null</jk>. 344 * @param actor Who is creating it. 345 * @return The stored order. 346 */ 347 public Order createOrder(Order order, String actor) { 348 if (order == null) 349 throw iaex("Order must not be null"); 350 var id = nextOrderId.incrementAndGet(); 351 order.setId(id); 352 orders.put(id, order); 353 recordAudit(actor, ENTITY_ORDER, String.valueOf(id), ACTION_CREATE, String.valueOf(order.getStatus())); 354 return order; 355 } 356 357 /** 358 * Updates an existing order. 359 * 360 * @param order The order to update. Must not be <jk>null</jk>; must carry an ID matching an existing order. 361 * @return The updated order. 362 * @throws PetstoreNotFoundException If no order with the given ID exists. 363 */ 364 public Order updateOrder(Order order) { 365 return updateOrder(order, ACTOR_API); 366 } 367 368 /** 369 * Replaces an order and records an {@code UPDATE} audit entry. 370 * 371 * @param order The order. Must not be <jk>null</jk>. 372 * @param actor Who is updating it. 373 * @return The stored order. 374 * @throws PetstoreNotFoundException If no order with the given ID exists. 375 */ 376 public Order updateOrder(Order order, String actor) { 377 if (order == null) 378 throw iaex("Order must not be null"); 379 if (! orders.containsKey(order.getId())) 380 throw new PetstoreNotFoundException("Order not found: id=" + order.getId()); 381 orders.put(order.getId(), order); 382 recordAudit(actor, ENTITY_ORDER, String.valueOf(order.getId()), ACTION_UPDATE, String.valueOf(order.getStatus())); 383 return order; 384 } 385 386 /** 387 * Deletes an order by ID. 388 * 389 * @param id The order ID. 390 * @throws PetstoreNotFoundException If no order with the given ID exists. 391 */ 392 public void deleteOrder(long id) { 393 deleteOrder(id, ACTOR_API); 394 } 395 396 /** 397 * Deletes an order and records a {@code DELETE} audit entry. 398 * 399 * @param id The order ID. 400 * @param actor Who is deleting it. 401 * @throws PetstoreNotFoundException If no order with the given ID exists. 402 */ 403 public void deleteOrder(long id, String actor) { 404 deleteOrder(id, actor, null); 405 } 406 407 /** 408 * Cancels an order, recording the reason as the {@code DELETE} audit detail. 409 * 410 * @param id The order ID. 411 * @param actor Who is cancelling it. 412 * @param reason Why, or <jk>null</jk> or blank for none (the audit detail is then the order status). 413 * @throws PetstoreNotFoundException If no order with the given ID exists. 414 */ 415 public void deleteOrder(long id, String actor, String reason) { 416 var removed = orders.remove(id); 417 if (removed == null) 418 throw new PetstoreNotFoundException("Order not found: id=" + id); 419 var detail = reason == null || reason.isBlank() ? String.valueOf(removed.getStatus()) : "cancelled: " + reason; 420 recordAudit(actor, ENTITY_ORDER, String.valueOf(id), ACTION_DELETE, detail); 421 } 422 423 //------------------------------------------------------------------------------------------------------------------ 424 // Users 425 //------------------------------------------------------------------------------------------------------------------ 426 427 /** 428 * Returns all users. 429 * 430 * @return All users, in arbitrary order. Never <jk>null</jk>. 431 */ 432 public Collection<User> getUsers() { 433 return List.copyOf(users.values()); 434 } 435 436 /** 437 * Returns a user by username. 438 * 439 * @param username The username (primary key). 440 * @return The user, or <jk>null</jk> if not found. 441 */ 442 public User getUser(String username) { 443 return users.get(username); 444 } 445 446 /** 447 * Creates a new user. 448 * 449 * @param user The user to create. Must not be <jk>null</jk>; must carry a non-null username not already in use. 450 * @return The created user. 451 * @throws IllegalArgumentException If the username is already in use. 452 */ 453 public User createUser(User user) { 454 return createUser(user, ACTOR_API); 455 } 456 457 /** 458 * Creates a user and records a {@code CREATE} audit entry. 459 * 460 * @param user The user. Must carry a non-null username not already in use. 461 * @param actor Who is creating it. 462 * @return The stored user. 463 * @throws IllegalArgumentException If the username is already in use. 464 */ 465 public User createUser(User user, String actor) { 466 if (user == null) 467 throw iaex("User must not be null"); 468 if (user.getUsername() == null) 469 throw iaex("User username must not be null"); 470 if (users.putIfAbsent(user.getUsername(), user) != null) 471 throw iaex("User already exists: username='%s'", user.getUsername()); 472 recordAudit(actor, "User", user.getUsername(), ACTION_CREATE, user.getUsername()); 473 return user; 474 } 475 476 /** 477 * Updates an existing user. 478 * 479 * @param user The user to update. Must not be <jk>null</jk>; must carry a username matching an existing user. 480 * @return The updated user. 481 * @throws PetstoreNotFoundException If no user with the given username exists. 482 */ 483 public User updateUser(User user) { 484 return updateUser(user, ACTOR_API); 485 } 486 487 /** 488 * Replaces a user and records an {@code UPDATE} audit entry. 489 * 490 * <p> 491 * The password is write-only and never read back, so a <jk>null</jk> or blank password on the replacement keeps the 492 * stored one. 493 * 494 * @param user The user. Must carry a username matching an existing user. 495 * @param actor Who is updating it. 496 * @return The stored user. 497 * @throws PetstoreNotFoundException If no user with the given username exists. 498 */ 499 public User updateUser(User user, String actor) { 500 if (user == null) 501 throw iaex("User must not be null"); 502 if (user.getUsername() == null) 503 throw iaex("User username must not be null"); 504 if (! users.containsKey(user.getUsername())) 505 throw new PetstoreNotFoundException("User not found: username=" + user.getUsername()); 506 var pw = user.getPassword(); 507 if (ib(pw)) 508 user.setPassword(users.get(user.getUsername()).getPassword()); 509 users.put(user.getUsername(), user); 510 recordAudit(actor, "User", user.getUsername(), ACTION_UPDATE, user.getUsername()); 511 return user; 512 } 513 514 /** 515 * Deletes a user by username. 516 * 517 * @param username The username. 518 * @throws PetstoreNotFoundException If no user with the given username exists. 519 */ 520 public void deleteUser(String username) { 521 deleteUser(username, ACTOR_API); 522 } 523 524 /** 525 * Deletes a user and records a {@code DELETE} audit entry. 526 * 527 * @param username The username. 528 * @param actor Who is deleting it. 529 * @throws PetstoreNotFoundException If no user with the given username exists. 530 */ 531 public void deleteUser(String username, String actor) { 532 if (users.remove(username) == null) 533 throw new PetstoreNotFoundException("User not found: username=" + username); 534 recordAudit(actor, "User", username, ACTION_DELETE, username); 535 } 536 537 //------------------------------------------------------------------------------------------------------------------ 538 // Audit 539 //------------------------------------------------------------------------------------------------------------------ 540 541 /** 542 * Returns the audit trail. 543 * 544 * @return An immutable snapshot, oldest first. Never <jk>null</jk>. 545 */ 546 public List<AuditEntry> getAudit() { 547 return List.copyOf(audit); 548 } 549 550 /** 551 * Appends an audit entry stamped with this store's clock. 552 * 553 * <p> 554 * Public so console operations that are not plain CRUD (restock, apply/discard pending changes) are audited too. 555 * 556 * @param actor Who did it. 557 * @param entity The entity kind. 558 * @param entityId The entity id. 559 * @param action The action. 560 * @param detail A short detail, or <jk>null</jk>. 561 * @return The new entry. 562 */ 563 public AuditEntry recordAudit(String actor, String entity, String entityId, String action, String detail) { 564 // Id assignment and append are one step so the trail stays in id order under concurrent writes. 565 synchronized (audit) { 566 var e = new AuditEntry().setId(nextAuditId.incrementAndGet()).setAt(clock.instant()) 567 .setActor(actor).setEntity(entity).setEntityId(entityId).setAction(action).setDetail(detail); 568 audit.add(e); 569 return e; 570 } 571 } 572 573 /** 574 * Returns the staging area for console inline edits (pet detail to Pending changes). 575 * 576 * @return The pending-changes staging area. Never <jk>null</jk>. 577 */ 578 public PendingChanges pendingChanges() { 579 return pendingChanges; 580 } 581 582 /** 583 * Bulk-loads rows with their ids preserved, writing no audit. Used by {@code PetstoreSeed}. 584 * 585 * <p> 586 * Id counters advance past the highest loaded id, so later creates never collide. 587 * 588 * @param newPets Pets to add or replace. 589 * @param newOrders Orders to add or replace. 590 * @param newUsers Users to add or replace. 591 * @param newAudit Audit rows to append, in order. 592 */ 593 public void load(Collection<Pet> newPets, Collection<Order> newOrders, Collection<User> newUsers, Collection<AuditEntry> newAudit) { 594 for (var p : newPets) { 595 pets.put(p.getId(), p); 596 nextPetId.accumulateAndGet(p.getId(), Math::max); 597 } 598 for (var o : newOrders) { 599 orders.put(o.getId(), o); 600 nextOrderId.accumulateAndGet(o.getId(), Math::max); 601 } 602 for (var u : newUsers) 603 users.put(u.getUsername(), u); 604 synchronized (audit) { 605 for (var a : newAudit) { 606 audit.add(a); 607 nextAuditId.accumulateAndGet(a.getId(), Math::max); 608 } 609 } 610 } 611 612 //------------------------------------------------------------------------------------------------------------------ 613 // BeanQuery views 614 //------------------------------------------------------------------------------------------------------------------ 615 616 /** 617 * Opens a query session over a snapshot of the pets. 618 * 619 * <h5 class='section'>Example:</h5> 620 * <p class='bjava'> 621 * <jk>try</jk> (<jk>var</jk> <jv>s</jv> = <jv>store</jv>.queryPets()) { 622 * <jk>return</jk> DataTablesQuery.<jsm>run</jsm>(<jv>req</jv>, <jv>s</jv>); 623 * } 624 * </p> 625 * 626 * @return A new session; close it. 627 */ 628 public InMemoryBeanQuerySession<Pet> queryPets() { return PET_QUERY.getSession(getPets()); } 629 630 /** @return A new session over a snapshot of the orders; close it. */ 631 public InMemoryBeanQuerySession<Order> queryOrders() { return ORDER_QUERY.getSession(getOrders()); } 632 633 /** @return A new session over a snapshot of the users; close it. */ 634 public InMemoryBeanQuerySession<User> queryUsers() { return USER_QUERY.getSession(getUsers()); } 635 636 /** @return A new session over a snapshot of the audit trail; close it. */ 637 public InMemoryBeanQuerySession<AuditEntry> queryAudit() { return AUDIT_QUERY.getSession(getAudit()); } 638 639 /** @return A new session over a snapshot of the staged changes; close it. */ 640 public InMemoryBeanQuerySession<PendingChange> queryPendingChanges() { return CHANGE_QUERY.getSession(pendingChanges.list()); } 641}