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}