@@ -273,6 +273,16 @@ public class ApiClientBase implements Cloneable, Closeable { | |||
return new RestResponse(response); | |||
} | |||
public RestResponse uploadStream(String path, InputStream in, String name, String method) throws Exception { | |||
final String url = getUrl(path, getBaseUri()); | |||
final NameAndValue[] headers = { new NameAndValue(getTokenHeader(), token) }; | |||
final HttpRequestBean request = new HttpRequestBean(method, url, in, name, headers); | |||
final HttpResponseBean response = HttpUtil.getStreamResponse(request); | |||
return new RestResponse(response); | |||
} | |||
public <T> T post(String path, T request) throws Exception { | |||
if (request instanceof ModelEntity) { | |||
return (T) post(path, ((ModelEntity) request).getEntity(), ((ModelEntity) request).getEntity().getClass()); | |||
@@ -353,6 +363,9 @@ public class ApiClientBase implements Cloneable, Closeable { | |||
} else if (path.startsWith("/") && clientUri.endsWith("/")) { | |||
path = path.substring(1); // caller has supplied a relative path | |||
} | |||
if (!path.startsWith("/") && !clientUri.endsWith("/")) { | |||
clientUri = clientUri + "/"; // client URI does not end with a slash, add one | |||
} | |||
return clientUri + path; | |||
} | |||
@@ -29,12 +29,6 @@ This code is available under the Apache License, version 2: http://www.apache.or | |||
<version>${junit.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.googlecode.jmockit</groupId> | |||
<artifactId>jmockit</artifactId> | |||
<version>1.1</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.cobbzilla</groupId> | |||
<artifactId>restex</artifactId> | |||
@@ -1,240 +0,0 @@ | |||
package org.cobbzilla.wizardtest.time; | |||
import mockit.Mock; | |||
import mockit.MockClass; | |||
import mockit.Mockit; | |||
import java.lang.reflect.Method; | |||
import java.text.SimpleDateFormat; | |||
import java.util.Calendar; | |||
import java.util.Date; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||
/** | |||
* Found at: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/ | |||
* | |||
* Class changes the system time returned by {@link System#currentTimeMillis()} via JMockit weaving. | |||
* <p/> | |||
* Original System class can be restored any time calling {@link #reset()} method. There are a few ways how to specify modified system time: | |||
* <ul> | |||
* <li>setting ms offset via {@link #setOffset(long)} | |||
* <li>changing ms offset (relatively) via {@link #changeOffset(long)} | |||
* <li>setting new date, time or ISO date/time via {@link #setIsoDate(String)} | |||
* </ul> | |||
* <p/> | |||
* Any of these methods can be used through system properties (-D) this way (first property in this order is used, others ignored): | |||
* <ul> | |||
* <li>{@code -Dsystime.offset=1000} - shift by one second to the future (negative number can be used) | |||
* <li>{@code -Dsystime.millis=1000} - set system time one second after start of the era (1970...) | |||
* <li>{@code -Dsystime.iso=2000-01-01T00:00:47} - 47 seconds after beginning of the 2000, alternatively you can set only time (00:00:47, date stays current) or | |||
* only date (2000-01-01, current time) without 'T' in both cases. | |||
* </ul> | |||
* <p/> | |||
* There must be something that causes class load, otherwise nothing happens. In order to allow this without modifying the original program, one may use this | |||
* class as a main class with original main class as the first argument (they will be correctly shifted when served to the original class). If no relevant | |||
* property is specified via -D, nothing happens. In any case (programmatic or main class replacement) this class has to be on a classpath. For application | |||
* server usage this means it has to be in its system libraries, not in EAR/WAR that is not loaded during the AS start yet. | |||
* <p/> | |||
* Example: | |||
* | |||
* <pre> | |||
* java -Dsystime.iso=2000-01-01T00:00:47 SystemTimeShifter my.uber.appserver.Main arg1 second "third long with spaces" | |||
* </pre> | |||
* <b>WARNING:</b> Sun/Oracle HotSpot JVM and its inline optimization may mess up with the mock after it is set up, so if you notice that the time | |||
* returns to normal after number of invocations, you should add {@code -XX:-Inline} option to your java command line. Other JVM specific options | |||
* may be needed for different JVM implementations. | |||
* | |||
* @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a> | |||
*/ | |||
public class SystemTimeShifter { | |||
/** | |||
* System property setting ms offset. | |||
*/ | |||
public static final String PROPERTY_OFFSET = "systime.offset"; | |||
/** | |||
* System property setting "current" millis. | |||
*/ | |||
public static final String PROPERTY_MILLIS = "systime.millis"; | |||
/** | |||
* System property setting ISO date/time (or date, or time). | |||
*/ | |||
public static final String PROPERTY_ISO_DATE = "systime.iso"; | |||
private static final long INIT_MILLIS = now(); | |||
private static final long INIT_NANOS = System.nanoTime(); | |||
private static long offset; | |||
private static boolean mockInstalled; | |||
@Deprecated | |||
protected SystemTimeShifter() { | |||
// prevents calls from subclass | |||
throw new UnsupportedOperationException(); | |||
} | |||
static { | |||
String isoDate = System.getProperty(PROPERTY_ISO_DATE); | |||
String millis = System.getProperty(PROPERTY_MILLIS); | |||
String offset = System.getProperty(PROPERTY_OFFSET); | |||
try { | |||
if (isoDate != null) { | |||
setIsoDate(isoDate); | |||
} else if (millis != null) { | |||
setMillis(Integer.parseInt(millis)); | |||
} else if (offset != null) { | |||
setOffset(Integer.parseInt(offset)); | |||
} | |||
} catch (NumberFormatException e) { | |||
e.printStackTrace(); | |||
} | |||
} | |||
/** | |||
* Bootstrap main to allow time shifting before actually loading the real main class. Real | |||
* main class must be the first argument, it will be removed from the list when calling the | |||
* real class. Without using any relevant -D property there will be no time shifting. | |||
* | |||
* @param args argument list with original (desired) class as the first argument | |||
* @throws Exception may happen during the reflection call of the other main | |||
*/ | |||
@SuppressWarnings({"unchecked", "rawtypes"}) | |||
public static void main(String[] args) throws Exception { | |||
String[] newArgs = new String[args.length - 1]; | |||
System.arraycopy(args, 1, newArgs, 0, args.length - 1); | |||
Class clazz = Class.forName(args[0]); | |||
Method main = clazz.getMethod("main", newArgs.getClass()); | |||
main.invoke(null, (Object) newArgs); | |||
} | |||
/** | |||
* Sets the new "system" time to specified ISO time. It is possible to set exact time with the format {@code yyyy-MM-dd'T'HH:mm:ss} (no apostrophes around T | |||
* in the actual string!) or one can set just time | |||
* (then current date stays) or just date (then current time stays). | |||
* <p/> | |||
* If parse fails for whatever reason, nothing is changed. | |||
* | |||
* @param isoDate String with ISO date (date+time, date or just time) | |||
*/ | |||
public static synchronized void setIsoDate(String isoDate) { | |||
try { | |||
if (isoDate.indexOf('T') != -1) { // it's date and time (so "classic" ISO timestamp) | |||
long wantedMillis = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(isoDate).getTime(); | |||
offset = wantedMillis - millisSinceClassInit() - INIT_MILLIS; | |||
} else if (isoDate.indexOf(':') != -1) { // it's just time we suppose | |||
Calendar calx = Calendar.getInstance(); | |||
calx.setTime(new SimpleDateFormat("HH:mm:ss").parse(isoDate)); | |||
Calendar cal = Calendar.getInstance(); | |||
cal.set(Calendar.HOUR_OF_DAY, calx.get(Calendar.HOUR_OF_DAY)); | |||
cal.set(Calendar.MINUTE, calx.get(Calendar.MINUTE)); | |||
cal.set(Calendar.SECOND, calx.get(Calendar.SECOND)); | |||
offset = cal.getTimeInMillis() - millisSinceClassInit() - INIT_MILLIS; | |||
} else { // it must be just date then! | |||
Calendar calx = Calendar.getInstance(); | |||
calx.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(isoDate)); | |||
Calendar cal = Calendar.getInstance(); | |||
cal.set(Calendar.DAY_OF_MONTH, calx.get(Calendar.DAY_OF_MONTH)); | |||
cal.set(Calendar.MONTH, calx.get(Calendar.MONTH)); | |||
cal.set(Calendar.YEAR, calx.get(Calendar.YEAR)); | |||
offset = cal.getTimeInMillis() - millisSinceClassInit() - INIT_MILLIS; | |||
} | |||
mockSystemClass(); | |||
} catch (Exception e) { | |||
e.printStackTrace(); | |||
} | |||
} | |||
/** | |||
* Sets ms offset against current millis (not against real, instead changes current value relatively). | |||
* | |||
* @param offset relative ms offset against "current" millis | |||
*/ | |||
public static synchronized void changeOffset(long offset) { | |||
SystemTimeShifter.offset += offset; | |||
mockSystemClass(); | |||
} | |||
/** | |||
* Sets ms offset against real millis (rewrites previous value). | |||
* | |||
* @param offset new absolute ms offset against real millis | |||
*/ | |||
public static synchronized void setOffset(long offset) { | |||
SystemTimeShifter.offset = offset; | |||
mockSystemClass(); | |||
} | |||
/** | |||
* Sets current millis to the specified value. | |||
* | |||
* @param timestamp new value of "current" millis | |||
*/ | |||
public static synchronized void setMillis(long timestamp) { | |||
offset = timestamp - INIT_MILLIS; | |||
mockSystemClass(); | |||
} | |||
/** | |||
* Resets the whole System time shifter and removes all JMockit stuff. Real system call is restored. | |||
*/ | |||
public static synchronized void reset() { | |||
Mockit.tearDownMocks(System.class); | |||
mockInstalled = false; | |||
offset = 0; | |||
System.out.println("Current time millis mock REMOVED"); | |||
} | |||
private static void mockSystemClass() { | |||
if (!mockInstalled) { | |||
Mockit.setUpMock(SystemMock.class); | |||
System.out.println("Current time millis mock INSTALLED: " + new Date()); | |||
mockInstalled = true; | |||
} else { | |||
System.out.println("Current time millis mock probably INSTALLED previously: " + new Date()); | |||
} | |||
} | |||
public static boolean isMockInstalled() { | |||
return mockInstalled; | |||
} | |||
/** | |||
* Handy if you set up the mock by some other means like {@link Mockit#setUpStartupMocks(Object...)}. | |||
* | |||
* @param mockInstalled true if you want to pretend that the mock is already in place (or is/will be installed otherwise) | |||
*/ | |||
public static void setMockInstalled(boolean mockInstalled) { | |||
SystemTimeShifter.mockInstalled = mockInstalled; | |||
} | |||
/** | |||
* Returns real time millis based on nano timer difference (not really a call to {@link System#currentTimeMillis()}. | |||
* | |||
* @return real time millis as close as possible | |||
*/ | |||
public static long currentRealTimeMillis() { | |||
return INIT_MILLIS + millisSinceClassInit(); | |||
} | |||
private static long millisSinceClassInit() { | |||
return (System.nanoTime() - INIT_NANOS) / 1000000; | |||
} | |||
@MockClass(realClass = System.class) | |||
public static class SystemMock { | |||
/** | |||
* Fake current time millis returns value modified by required offset. | |||
* | |||
* @return fake "current" millis | |||
*/ | |||
@Mock | |||
public static long currentTimeMillis() { | |||
return INIT_MILLIS + offset + millisSinceClassInit(); | |||
} | |||
} | |||
} |
@@ -1,73 +0,0 @@ | |||
package org.cobbzilla.wizard.cache.redis; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.string.StringUtil; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import java.util.List; | |||
import java.util.concurrent.TimeUnit; | |||
import static org.cobbzilla.wizard.cache.redis.RedisService.EX; | |||
import static org.cobbzilla.wizard.cache.redis.RedisService.NX; | |||
@Service @Slf4j | |||
public class ActivationCodeService { | |||
@Autowired @Getter @Setter private RedisService redis; | |||
public String peek(String key) { return redis.get_plaintext(key); } | |||
public boolean attempt(String key, String claimant) { | |||
try { | |||
final Long remaining = redis.decr(key); | |||
if (remaining == null || remaining < 0) return false; | |||
redis.lpush(getClaimantsKey(key), claimant); | |||
return true; | |||
} catch (Exception e) { | |||
log.warn("attempt("+key+") error: "+e); | |||
return false; | |||
} | |||
} | |||
public void define (String key, int quantity, long expirationSeconds) { | |||
redis.set_plaintext(key, String.valueOf(quantity), NX, EX, expirationSeconds); | |||
} | |||
public List<String> getClaimants (String key) { return redis.list(getClaimantsKey(key)); } | |||
private String getClaimantsKey(String key) { return key+"_claimed"; } | |||
/** | |||
* @param args [0] = key; [1] = quantity; [2] = expiration (# days); [3] = redis key (optional) | |||
*/ | |||
public static void main (final String[] args) { | |||
final RedisService redis = new RedisService(); | |||
final String redisKey = (args.length == 4) ? args[3] : null; | |||
redis.setConfiguration(() -> new RedisConfiguration(redisKey)); | |||
final ActivationCodeService acService = new ActivationCodeService(); | |||
acService.setRedis(redis); | |||
final String key = args[0]; | |||
if (args.length > 1) { | |||
final int quantity = Integer.parseInt(args[1]); | |||
final long expirationSeconds = Integer.parseInt(args[2]) * TimeUnit.DAYS.toSeconds(1); | |||
acService.define(key, quantity, expirationSeconds); | |||
System.out.println("successfully defined key: " + key); | |||
} else { | |||
System.out.println("key: " + key); | |||
System.out.println("remaining: " + acService.peek(key)); | |||
System.out.println("claimants: " + StringUtil.toString(acService.getClaimants(key), ", ")); | |||
} | |||
} | |||
} |
@@ -73,7 +73,7 @@ public class RedisService { | |||
} | |||
public void reconnect () { | |||
log.debug("marking redis for reconnection..."); | |||
if (log.isDebugEnabled()) log.debug("marking redis for reconnection..."); | |||
synchronized (redis) { | |||
if (redis.get() != null) { | |||
try { redis.get().disconnect(); } catch (Exception e) { | |||
@@ -87,7 +87,7 @@ public class RedisService { | |||
private Jedis getRedis () { | |||
synchronized (redis) { | |||
if (redis.get() == null) { | |||
log.debug("connecting to redis..."); | |||
if (log.isDebugEnabled()) log.debug("connecting to redis..."); | |||
redis.set(newJedis()); | |||
} | |||
} | |||
@@ -130,6 +130,17 @@ public class RedisService { | |||
public void set(String key, String value) { __set(key, value, 0, MAX_RETRIES); } | |||
public void set_plaintext(String key, String value, String nxxx, String expx, long time) { | |||
__set_plaintext(key, value, nxxx, expx, time, 0, MAX_RETRIES); | |||
} | |||
public void set_plaintext(String key, String value, String expx, long time) { | |||
__set_plaintext(key, value, XX, expx, time, 0, MAX_RETRIES); | |||
__set_plaintext(key, value, NX, expx, time, 0, MAX_RETRIES); | |||
} | |||
public void set_plaintext(String key, String value) { __set_plaintext(key, value, 0, MAX_RETRIES); } | |||
public void setAll(Collection<String> keys, String value, String expx, long time) { | |||
for (String k : keys) set(k, value, expx, time); | |||
} | |||
@@ -164,14 +175,6 @@ public class RedisService { | |||
public Long del(String key) { return __del(key, 0, MAX_RETRIES); } | |||
public Long del_withPrefix(String prefixedKey) { return __del(prefixedKey, 0, MAX_RETRIES, false); } | |||
public void set_plaintext(String key, String value, String nxxx, String expx, long time) { | |||
__set(key, value, nxxx, expx, time, 0, MAX_RETRIES); | |||
} | |||
public void set_plaintext(String key, String value) { | |||
__set(key, value, 0, MAX_RETRIES); | |||
} | |||
public Long sadd(String key, String value) { return sadd(key, new String[]{value}); } | |||
public Long sadd(String key, String[] values) { return __sadd(key, values, 0, MAX_RETRIES); } | |||
@@ -217,18 +220,21 @@ public class RedisService { | |||
} | |||
public String lock(String key, long lockTimeout, long deadlockTimeout) { | |||
if (log.isDebugEnabled()) log.debug("lock("+key+") starting"); | |||
key = key + LOCK_SUFFIX; | |||
final String uuid = UUID.randomUUID().toString(); | |||
String lockVal = get(key); | |||
final long start = now(); | |||
while ((lockVal == null || !lockVal.equals(uuid)) && (now() - start < lockTimeout)) { | |||
set(key, uuid, NX, EX, deadlockTimeout/1000); | |||
if (log.isDebugEnabled()) log.debug("lock("+key+") locked with uuid="+uuid); | |||
lockVal = get(key); | |||
if (log.isDebugEnabled()) log.debug("lock("+key+") after locking with uuid="+uuid+", lockVal="+lockVal); | |||
} | |||
if (lockVal == null || !lockVal.equals(uuid)) { | |||
return die("lock: timeout locking "+key); | |||
} | |||
log.info("lock: LOCKED "+key); | |||
if (log.isDebugEnabled()) log.debug("lock: LOCKED "+key+" = "+lockVal); | |||
return uuid; | |||
} | |||
@@ -372,6 +378,30 @@ public class RedisService { | |||
} | |||
} | |||
private String __set_plaintext(String key, String value, String nxxx, String expx, long time, int attempt, int maxRetries) { | |||
try { | |||
synchronized (redis) { | |||
return getRedis().set(prefix(key), value, nxxx, expx, time); | |||
} | |||
} catch (RuntimeException e) { | |||
if (attempt > maxRetries) throw e; | |||
resetForRetry(attempt, "retrying RedisService.__set_plaintext"); | |||
return __set_plaintext(key, value, nxxx, expx, time, attempt + 1, maxRetries); | |||
} | |||
} | |||
private String __set_plaintext(String key, String value, int attempt, int maxRetries) { | |||
try { | |||
synchronized (redis) { | |||
return getRedis().set(prefix(key), value); | |||
} | |||
} catch (RuntimeException e) { | |||
if (attempt > maxRetries) throw e; | |||
resetForRetry(attempt, "retrying RedisService.__set_plaintext"); | |||
return __set_plaintext(key, value, attempt+1, maxRetries); | |||
} | |||
} | |||
private Long __lpush(String key, String value, int attempt, int maxRetries) { | |||
try { | |||
synchronized (redis) { | |||
@@ -6,6 +6,7 @@ package org.cobbzilla.wizard.dao; | |||
*/ | |||
import lombok.Getter; | |||
import lombok.NonNull; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.reflect.ReflectionUtil; | |||
import org.cobbzilla.wizard.api.CrudOperation; | |||
@@ -26,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.orm.hibernate4.HibernateTemplate; | |||
import org.springframework.transaction.annotation.Transactional; | |||
import javax.annotation.Nullable; | |||
import javax.validation.Valid; | |||
import javax.validation.constraints.NotNull; | |||
import java.util.*; | |||
@@ -34,6 +36,7 @@ import java.util.concurrent.atomic.AtomicInteger; | |||
import java.util.concurrent.atomic.AtomicLong; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import static com.google.common.base.Preconditions.checkNotNull; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
@@ -252,15 +255,22 @@ public abstract class AbstractCRUDDAO<E extends Identifiable> | |||
} | |||
} | |||
public int bulkUpdate(String setField, Object setValue) { | |||
public int bulkUpdate(@NonNull final String setField, @Nullable final Object setValue) { | |||
return bulkUpdate(setField, setValue, (String[]) null, null); | |||
} | |||
public int bulkUpdate(String setField, Object setValue, String whereField, Object whereValue) { | |||
return bulkUpdate(setField, setValue, new String[] {whereField}, new Object[] {whereValue}); | |||
public int bulkUpdate(@NonNull final String setField, @Nullable final Object setValue, | |||
@NonNull final String whereField, @Nullable final Object whereValue) { | |||
return bulkUpdate(setField, setValue, new String[] { whereField }, new Object[] { whereValue }); | |||
} | |||
public int bulkUpdate(String setField, Object setValue, String[] whereFields, Object[] whereValues) { | |||
public int bulkUpdate(@NonNull final String setField, @Nullable final Object setValue, | |||
@Nullable final String[] whereFields, @Nullable final Object[] whereValues) { | |||
return bulkUpdate(new String[] { setField }, new Object[] { setValue }, whereFields, whereValues); | |||
} | |||
public int bulkUpdate(@NonNull final String[] setFields, @Nullable final Object[] setValues, | |||
@Nullable final String[] whereFields, @Nullable final Object[] whereValues) { | |||
final Session session = getHibernateTemplate().getSessionFactory().getCurrentSession(); | |||
final String whereClause; | |||
final boolean hasWhereClause = !empty(whereFields); | |||
@@ -282,13 +292,24 @@ public abstract class AbstractCRUDDAO<E extends Identifiable> | |||
if (!empty(whereValues)) return die("bulkUpdate: number of whereFields did not match number of whereValues"); | |||
whereClause = ""; | |||
} | |||
final Query queryBase; | |||
if (setValue == null) { | |||
queryBase = session.createQuery("UPDATE " + getEntityClass().getSimpleName() + " SET " + setField + " = NULL"+whereClause); | |||
if (empty(setValues)) { | |||
queryBase = session.createQuery("UPDATE " + getEntityClass().getSimpleName() | |||
+ " SET " + String.join(" IS NULL, ", setFields) + " IS NULL" | |||
+ whereClause); | |||
} else { | |||
queryBase = session.createQuery("UPDATE " + getEntityClass().getSimpleName() + " SET " + setField + " = :" + setField+whereClause) | |||
.setParameter(setField, setValue); | |||
final var setFieldsSQLPart = Arrays.stream(setFields) | |||
.map(s -> s + (s == null ? " IS NULL" : " = :" + s)) | |||
.collect(Collectors.joining(", ")); | |||
queryBase = session.createQuery("UPDATE " + getEntityClass().getSimpleName() | |||
+ " SET " + setFieldsSQLPart | |||
+ whereClause); | |||
for (var i = 0; i < setValues.length; i++) { | |||
if (setValues[i] != null) queryBase.setParameter(setFields[i], setValues[i]); | |||
} | |||
} | |||
final Query query; | |||
if (hasWhereClause) { | |||
Query q = queryBase; | |||