|
| 1 | +package io.ebean.test; |
| 2 | + |
| 3 | +import io.ebeaninternal.server.deploy.BeanProperty; |
| 4 | + |
| 5 | +import java.math.BigDecimal; |
| 6 | +import java.math.RoundingMode; |
| 7 | +import java.time.Instant; |
| 8 | +import java.time.LocalDate; |
| 9 | +import java.time.LocalDateTime; |
| 10 | +import java.time.OffsetDateTime; |
| 11 | +import java.time.ZonedDateTime; |
| 12 | +import java.util.UUID; |
| 13 | +import java.util.concurrent.ThreadLocalRandom; |
| 14 | + |
| 15 | +/** |
| 16 | + * Generates random values for entity bean properties in tests. |
| 17 | + * <p> |
| 18 | + * The primary entry point is {@link #generate(BeanProperty)}, which is |
| 19 | + * property-aware (e.g. caps String values at the column's max length). |
| 20 | + * The secondary entry point {@link #generate(Class)} works on the Java type |
| 21 | + * alone and is useful when no property metadata is available. |
| 22 | + * </p> |
| 23 | + * <p> |
| 24 | + * All per-type factory methods ({@link #randomString(String, int)}, |
| 25 | + * {@link #randomBigDecimal(int, int)}, {@link #randomLong()}, etc.) are |
| 26 | + * {@code protected} so that subclasses can override individual types without |
| 27 | + * replacing the full dispatch logic. |
| 28 | + * </p> |
| 29 | + * <p> |
| 30 | + * Returns {@code null} for types that are not mapped (exotic / unknown types) — |
| 31 | + * the caller is expected to set those fields manually. |
| 32 | + * </p> |
| 33 | + */ |
| 34 | +public class RandomValueGenerator { |
| 35 | + |
| 36 | + /** |
| 37 | + * Generate a random value for the given bean property. |
| 38 | + * <p> |
| 39 | + * For {@code String} properties, the value is capped at the column's |
| 40 | + * {@link BeanProperty#dbLength()} when that length is positive. |
| 41 | + * For {@code BigDecimal} properties, precision and scale from the column |
| 42 | + * definition are used. |
| 43 | + * </p> |
| 44 | + */ |
| 45 | + public Object generate(BeanProperty prop) { |
| 46 | + Class<?> type = prop.type(); |
| 47 | + if (type == String.class) { |
| 48 | + return randomString(prop.name(), prop.dbLength()); |
| 49 | + } |
| 50 | + if (type == BigDecimal.class) { |
| 51 | + return randomBigDecimal(prop.dbLength(), prop.dbScale()); |
| 52 | + } |
| 53 | + return generate(type); |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Generate a random value for the given Java type, without property metadata. |
| 58 | + * <p> |
| 59 | + * String values produced here use a fixed 8-character length. Use |
| 60 | + * {@link #generate(BeanProperty)} when column-length constraints matter. |
| 61 | + * </p> |
| 62 | + */ |
| 63 | + @SuppressWarnings({"unchecked", "rawtypes"}) |
| 64 | + public Object generate(Class<?> type) { |
| 65 | + if (type == null) return null; |
| 66 | + if (type == String.class) return randomString(null, 0); |
| 67 | + if (type == Boolean.class || type == boolean.class) return randomBoolean(); |
| 68 | + if (type == UUID.class) return randomUUID(); |
| 69 | + if (type == Instant.class) return randomInstant(); |
| 70 | + if (type == OffsetDateTime.class) return randomOffsetDateTime(); |
| 71 | + if (type == ZonedDateTime.class) return randomZonedDateTime(); |
| 72 | + if (type == LocalDate.class) return randomLocalDate(); |
| 73 | + if (type == LocalDateTime.class) return randomLocalDateTime(); |
| 74 | + if (type == Long.class || type == long.class) return randomLong(); |
| 75 | + if (type == Integer.class || type == int.class) return randomInt(); |
| 76 | + if (type == Short.class || type == short.class) return randomShort(); |
| 77 | + if (type == BigDecimal.class) return randomBigDecimal(0, -1); |
| 78 | + if (type == Double.class || type == double.class) return randomDouble(); |
| 79 | + if (type == Float.class || type == float.class) return randomFloat(); |
| 80 | + if (type.isEnum()) return randomEnum(type); |
| 81 | + return null; // exotic/unknown type — caller must set this field manually |
| 82 | + } |
| 83 | + |
| 84 | + /** Return a random {@code long} in [1, 100_000). */ |
| 85 | + protected long randomLong() { |
| 86 | + return ThreadLocalRandom.current().nextLong(1, 100_000L); |
| 87 | + } |
| 88 | + |
| 89 | + /** Return a random {@code int} in [1, 1_000). */ |
| 90 | + protected int randomInt() { |
| 91 | + return ThreadLocalRandom.current().nextInt(1, 1_000); |
| 92 | + } |
| 93 | + |
| 94 | + /** Return a random {@code short} in [1, 100). */ |
| 95 | + protected short randomShort() { |
| 96 | + return (short) ThreadLocalRandom.current().nextInt(1, 100); |
| 97 | + } |
| 98 | + |
| 99 | + /** Return a random {@code boolean} — defaults to {@code true}. */ |
| 100 | + protected boolean randomBoolean() { |
| 101 | + return true; |
| 102 | + } |
| 103 | + |
| 104 | + /** Return a random {@link UUID}. */ |
| 105 | + protected UUID randomUUID() { |
| 106 | + return UUID.randomUUID(); |
| 107 | + } |
| 108 | + |
| 109 | + /** Return a random {@link Instant} — defaults to now. */ |
| 110 | + protected Instant randomInstant() { |
| 111 | + return Instant.now(); |
| 112 | + } |
| 113 | + |
| 114 | + /** Return a random {@link OffsetDateTime} — defaults to now. */ |
| 115 | + protected OffsetDateTime randomOffsetDateTime() { |
| 116 | + return OffsetDateTime.now(); |
| 117 | + } |
| 118 | + |
| 119 | + /** Return a random {@link ZonedDateTime} — defaults to now. */ |
| 120 | + protected ZonedDateTime randomZonedDateTime() { |
| 121 | + return ZonedDateTime.now(); |
| 122 | + } |
| 123 | + |
| 124 | + /** Return a random {@link LocalDate} — defaults to today. */ |
| 125 | + protected LocalDate randomLocalDate() { |
| 126 | + return LocalDate.now(); |
| 127 | + } |
| 128 | + |
| 129 | + /** Return a random {@link LocalDateTime} — defaults to now. */ |
| 130 | + protected LocalDateTime randomLocalDateTime() { |
| 131 | + return LocalDateTime.now(); |
| 132 | + } |
| 133 | + |
| 134 | + /** Return a random {@code double} in [1, 100). */ |
| 135 | + protected double randomDouble() { |
| 136 | + return ThreadLocalRandom.current().nextDouble(1, 100); |
| 137 | + } |
| 138 | + |
| 139 | + /** Return a random {@code float} in [1, 100). */ |
| 140 | + protected float randomFloat() { |
| 141 | + return (float) ThreadLocalRandom.current().nextDouble(1, 100); |
| 142 | + } |
| 143 | + |
| 144 | + /** |
| 145 | + * Return a value for the given enum type — defaults to the first declared constant. |
| 146 | + * Returns {@code null} if the enum has no constants. |
| 147 | + */ |
| 148 | + @SuppressWarnings({"unchecked", "rawtypes"}) |
| 149 | + protected Object randomEnum(Class<?> type) { |
| 150 | + Object[] constants = type.getEnumConstants(); |
| 151 | + return constants.length > 0 ? constants[0] : null; |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * Generate a random string, optionally capped at {@code maxLength}. |
| 156 | + * <p> |
| 157 | + * When the property name contains "email" (case-insensitive), a value of the |
| 158 | + * form {@code <prefix>@domain.com} is returned, truncated to fit {@code maxLength}. |
| 159 | + * Otherwise a UUID-derived string is returned, truncated to {@code maxLength} |
| 160 | + * (defaulting to 8 characters when {@code maxLength} is 0 or negative). |
| 161 | + * </p> |
| 162 | + */ |
| 163 | + protected String randomString(String propName, int maxLength) { |
| 164 | + String base = UUID.randomUUID().toString().replace("-", ""); // 32 chars |
| 165 | + if (propName != null && propName.toLowerCase().contains("email")) { |
| 166 | + String email = base.substring(0, 8) + "@domain.com"; |
| 167 | + if (maxLength > 0 && email.length() > maxLength) { |
| 168 | + int localLen = maxLength - "@domain.com".length(); |
| 169 | + email = (localLen > 0 ? base.substring(0, localLen) : base.substring(0, 1)) + "@domain.com"; |
| 170 | + } |
| 171 | + return email; |
| 172 | + } |
| 173 | + if (maxLength <= 0) { |
| 174 | + return base.substring(0, 8); |
| 175 | + } |
| 176 | + int len = Math.min(maxLength, base.length()); |
| 177 | + return base.substring(0, len); |
| 178 | + } |
| 179 | + |
| 180 | + /** |
| 181 | + * Generate a random {@link BigDecimal} that fits within the given precision and scale. |
| 182 | + * <p> |
| 183 | + * {@code precision} is the total number of significant digits ({@code dbLength}); |
| 184 | + * {@code scale} is the number of decimal places ({@code dbScale}). |
| 185 | + * When precision is 0 or negative (unknown), a default of 6 integer digits is used. |
| 186 | + * When scale is negative (unknown), a default scale of 2 is used. |
| 187 | + * </p> |
| 188 | + */ |
| 189 | + protected BigDecimal randomBigDecimal(int precision, int scale) { |
| 190 | + int actualScale = scale >= 0 ? scale : 2; |
| 191 | + int intDigits = precision > 0 ? Math.max(1, precision - actualScale) : 6; |
| 192 | + long maxInt = (long) Math.pow(10, intDigits) - 1; |
| 193 | + long intPart = ThreadLocalRandom.current().nextLong(1, maxInt + 1); |
| 194 | + if (actualScale == 0) { |
| 195 | + return BigDecimal.valueOf(intPart); |
| 196 | + } |
| 197 | + long scaleFactor = (long) Math.pow(10, actualScale); |
| 198 | + long fracPart = ThreadLocalRandom.current().nextLong(0, scaleFactor); |
| 199 | + double value = intPart + (double) fracPart / scaleFactor; |
| 200 | + return BigDecimal.valueOf(value).setScale(actualScale, RoundingMode.HALF_UP); |
| 201 | + } |
| 202 | +} |
0 commit comments