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.data;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.time.*;
022import java.time.format.*;
023import java.util.*;
024
025import org.apache.juneau.petstore.dto.*;
026import org.apache.juneau.petstore.service.*;
027
028/**
029 * Deterministic demo data for the petstore console: about 500 pets, 2,000 orders, 50 users and 300 audit rows,
030 * generated from a fixed {@link Random} seed and a fixed {@link Clock} so tests and screenshots are repeatable.
031 *
032 * <p>
033 * The classic rows a {@code new PetStore()} loads (pets 1-9, orders 101-103, three users) stay first with their ids;
034 * generated rows follow.  Both runners seed this way on startup.
035 *
036 * <h5 class='section'>Example:</h5>
037 * <p class='bjava'>
038 *    <jc>// Default seed and clock (what the runners use).</jc>
039 *    PetStore <jv>store</jv> = PetstoreSeed.<jsm>create</jsm>().populate(<jk>new</jk> PetStore());
040 *
041 *    <jc>// A different but still repeatable data set.</jc>
042 *    PetStore <jv>other</jv> = PetstoreSeed.<jsm>create</jsm>()
043 *       .randomSeed(42L)
044 *       .clock(Clock.<jsm>fixed</jsm>(Instant.<jsm>parse</jsm>(<js>"2027-01-01T00:00:00Z"</js>), ZoneOffset.<jsf>UTC</jsf>))
045 *       .populate(<jk>new</jk> PetStore());
046 * </p>
047 */
048public final class PetstoreSeed {
049
050   /** Default random seed. */
051   public static final long DEFAULT_SEED = 20261004L;
052
053   /** Default clock: fixed at 2026-10-01T12:00:00Z, UTC. */
054   public static final Clock DEFAULT_CLOCK = Clock.fixed(Instant.parse("2026-10-01T12:00:00Z"), ZoneOffset.UTC);
055
056   /** Environment variable that pins the petstore's "now"; see {@link #fixedNowClock()}. */
057   public static final String FIXED_NOW_ENV = "PETSTORE_FIXED_NOW";
058
059   /** Total pets after {@link #populate(PetStore)}. */
060   public static final int PETS = 500;
061   /** Total orders after {@link #populate(PetStore)}. */
062   public static final int ORDERS = 2000;
063   /** Total users after {@link #populate(PetStore)}. */
064   public static final int USERS = 50;
065   /** Audit rows written by {@link #populate(PetStore)}. */
066   public static final int AUDIT = 300;
067
068   private static final String[] NAMES = {
069      "Biscuit", "Pepper", "Mango", "Nori", "Juniper", "Clover", "Pickle", "Waffles", "Sprout", "Maple",
070      "Pebble", "Olive", "Tofu", "Ziggy", "Hazel", "Basil", "Comet", "Dot", "Fig", "Rusty"
071   };
072   private static final String[] FIRST = { "Ana", "Ben", "Cleo", "Dev", "Eli", "Fay", "Gus", "Hana", "Ivo", "Jo" };
073   private static final String[] LAST = { "Abbott", "Brooks", "Chen", "Diaz", "Evans", "Fischer", "Gray", "Hale" };
074   private static final String USER_FMT = "user%02d";
075   private static final String[] ENTITIES = { "Pet", "Order", "User" };
076   private static final String[] ACTIONS = { "CREATE", "UPDATE", "DELETE" };
077
078   private long seed = DEFAULT_SEED;
079   private Clock clock = DEFAULT_CLOCK;
080
081   private PetstoreSeed() {}
082
083   /** @return A new seed builder with the default seed and clock. */
084   public static PetstoreSeed create() {
085      return new PetstoreSeed();
086   }
087
088   /**
089    * @param value The {@link Random} seed.
090    * @return This object.
091    */
092   public PetstoreSeed randomSeed(long value) {
093      seed = value;
094      return this;
095   }
096
097   /**
098    * The clock the {@value #FIXED_NOW_ENV} environment variable pins "now" to, so screenshots of the day-relative store
099    * and jobs pages repeat from run to run.
100    *
101    * @return The clock, or <jk>null</jk> when the variable is unset, meaning the system clock.
102    */
103   public static Clock fixedNowClock() {
104      return fixedNowClock(System.getenv(FIXED_NOW_ENV));
105   }
106
107   /** @return The pinned "now" when {@value #FIXED_NOW_ENV} is set, else the current instant. */
108   public static Instant now() {
109      var c = fixedNowClock();
110      return c == null ? Instant.now() : c.instant();
111   }
112
113   /**
114    * @param value The variable's value: an ISO-8601 instant such as {@code 2026-10-01T12:00:00Z}, {@code seed} for
115    *    {@link #DEFAULT_CLOCK}, or <jk>null</jk> or blank for none.
116    * @return The clock it pins "now" to, or <jk>null</jk> for none.
117    */
118   static Clock fixedNowClock(String value) {
119      if (ib(value))
120         return null;
121      var v = value.trim();
122      return "seed".equals(v) ? DEFAULT_CLOCK : Clock.fixed(Instant.parse(v), ZoneOffset.UTC);
123   }
124
125   /**
126    * @param value The clock that dates orders and audit rows.  Must not be <jk>null</jk>.
127    * @return This object.
128    */
129   public PetstoreSeed clock(Clock value) {
130      clock = Objects.requireNonNull(value, "value");
131      return this;
132   }
133
134   /**
135    * Adds the generated rows to a store that holds only the classic rows.
136    *
137    * @param store A freshly constructed store.
138    * @return The same store, for chaining.
139    * @throws IllegalArgumentException If the store already holds more than the classic rows.
140    */
141   @SuppressWarnings({
142      "java:S2245" // Demo seed data only; a fixed-seed Random is required for repeatability and is not security-sensitive.
143   })
144   public PetStore populate(PetStore store) {
145      var existing = store.getPets().size();
146      if (existing > 9)
147         throw iaex("PetstoreSeed.populate expects a store holding only the classic rows; found '%s' pets", existing);
148      var r = new Random(seed);
149      var now = clock.instant();
150      var species = Species.values();
151      var orderStatus = OrderStatus.values();
152
153      var pets = new ArrayList<Pet>();
154      for (long id = 10; id <= PETS; id++) {
155         var sp = species[(int)(id % species.length)];
156         var roll = r.nextInt(10);
157         var status = petStatus(roll);
158         pets.add(new Pet().setId(id).setSpecies(sp).setStatus(status)
159            .setName(NAMES[r.nextInt(NAMES.length)] + " " + id)
160            .setPrice(5 + r.nextInt(9_500) / 100f)
161            .setTags(List.of(sp.name().toLowerCase(Locale.ROOT))));
162      }
163
164      var users = new ArrayList<User>();
165      for (var n = 4; n <= USERS; n++) {
166         var username = f(USER_FMT, n);
167         users.add(new User().setUsername(username)
168            .setFirstName(FIRST[r.nextInt(FIRST.length)]).setLastName(LAST[r.nextInt(LAST.length)])
169            .setEmail(username + "@example.org").setPassword("demo").setPhone(f("555-01%02d", n))
170            .setUserStatus(r.nextInt(10) == 0 ? UserStatus.INACTIVE : UserStatus.ACTIVE));
171      }
172
173      var day = DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneOffset.UTC);
174      var orders = new ArrayList<Order>();
175      for (long id = 104; id <= 2100; id++)
176         orders.add(new Order().setId(id).setPetId(1L + r.nextInt(PETS))
177            .setShipDate(day.format(now.minus(Duration.ofDays(r.nextInt(90)))))
178            .setStatus(orderStatus[r.nextInt(orderStatus.length)]));
179
180      var stamps = new ArrayList<Instant>();
181      for (var i = 0; i < AUDIT; i++)
182         stamps.add(now.minus(Duration.ofMinutes(1L + r.nextInt(90 * 24 * 60))));
183      stamps.sort(Comparator.naturalOrder());
184      var audit = new ArrayList<AuditEntry>();
185      for (var i = 0; i < AUDIT; i++) {
186         var kind = r.nextInt(3);
187         var entity = ENTITIES[kind];
188         var entityId = switch (kind) {
189            case 0 -> String.valueOf(1 + r.nextInt(PETS));
190            case 1 -> String.valueOf(101 + r.nextInt(ORDERS));
191            default -> f(USER_FMT, 4 + r.nextInt(USERS - 3));
192         };
193         var actor = r.nextInt(4) == 0 ? PetStore.ACTOR_API : "console:" + f(USER_FMT, 4 + r.nextInt(USERS - 3));
194         audit.add(new AuditEntry().setId(i + 1L).setAt(stamps.get(i)).setActor(actor)
195            .setEntity(entity).setEntityId(entityId).setAction(ACTIONS[r.nextInt(ACTIONS.length)]));
196      }
197
198      store.load(pets, orders, users, audit);
199      return store;
200   }
201
202   private static PetStatus petStatus(int roll) {
203      if (roll < 6)
204         return PetStatus.AVAILABLE;
205      return roll < 8 ? PetStatus.PENDING : PetStatus.SOLD;
206   }
207}