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}