Merge remote-tracking branch 'origin/master'

This commit is contained in:
fmittag committed 2014-05-08 16:08:15 +02:00
commit 8be675705b
36 files changed
+1414 -139

No files matched your search

@@ -1,6 +1,6 @@
package com.sap.sailing.domain.common;
public enum ScoringSchemeType {
LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE,
HIGH_POINT_FIRST_GETS_TEN, HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO
LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE, HIGH_POINT_FIRST_GETS_TEN,
HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO, HIGH_POINT_WINNER_GETS_SIX
}
@@ -0,0 +1,5 @@
package com.sap.sailing.domain.base;
public interface BoatChangeListener {
void sailIdChanged(String oldSailId, String newSailId);
}
@@ -10,4 +10,17 @@ public interface Competitor extends Named, WithID, IsManagedBySharedDomainFactor
Boat getBoat();
Color getColor();
/**
* Adds a listener to this competitor. The listener is also added to the boat and the team for changes.
* Adding a listener that is already part of this competitor's listeners set remains without effect.
*/
void addCompetitorChangeListener(CompetitorChangeListener listener);
/**
* Removes a listener from this competitor. The listener is also removed from the boat and the team.
* Trying to remove a listener that is not currently in the set of this competitor's listener remains
* without effect.
*/
void removeCompetitorChangeListener(CompetitorChangeListener listener);
}
@@ -0,0 +1,9 @@
package com.sap.sailing.domain.base;
import com.sap.sailing.domain.common.Color;
public interface CompetitorChangeListener extends BoatChangeListener, NationalityChangeListener {
void colorChanged(Color oldColor, Color newColor);
void nameChanged(String oldName, String newName);
}
@@ -0,0 +1,7 @@
package com.sap.sailing.domain.base;
import java.io.Serializable;
public interface NationalityChangeListener extends Serializable {
void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality);
}
@@ -1,17 +1,31 @@
package com.sap.sailing.domain.base.impl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.HashSet;
import java.util.Set;
import com.sap.sailing.domain.base.BoatChangeListener;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.impl.NamedImpl;
import com.sap.sailing.domain.common.impl.Util;
public class BoatImpl extends NamedImpl implements DynamicBoat {
private static final long serialVersionUID = 3489730487528955788L;
private final BoatClass boatClass;
private String sailID;
private transient Set<BoatChangeListener> listeners;
public BoatImpl(String name, BoatClass boatClass, String sailID) {
super(name);
this.boatClass = boatClass;
this.sailID = sailID;
this.listeners = new HashSet<BoatChangeListener>();
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
listeners = new HashSet<BoatChangeListener>();
}
@Override
@@ -26,6 +40,32 @@ public class BoatImpl extends NamedImpl implements DynamicBoat {
@Override
public void setSailId(String newSailId) {
this.sailID = newSailId;
final String oldSailId = this.sailID;
if (!Util.equalsWithNull(oldSailId, newSailId)) {
this.sailID = newSailId;
for (BoatChangeListener listener : getListeners()) {
listener.sailIdChanged(oldSailId, newSailId);
}
}
}
@Override
public void addBoatChangeListener(BoatChangeListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
@Override
public void removeCompetitorChangeListener(BoatChangeListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
private Iterable<BoatChangeListener> getListeners() {
synchronized (listeners) {
return new HashSet<BoatChangeListener>(listeners);
}
}
}
@@ -1,10 +1,16 @@
package com.sap.sailing.domain.base.impl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CompetitorChangeListener;
import com.sap.sailing.domain.base.SharedDomainFactory;
import com.sap.sailing.domain.common.Color;
import com.sap.sailing.domain.common.impl.Util;
public class CompetitorImpl implements DynamicCompetitor {
private static final long serialVersionUID = 294603681016643157L;
@@ -12,8 +18,8 @@ public class CompetitorImpl implements DynamicCompetitor {
private final DynamicBoat boat;
private final Serializable id;
private String name;
private Color color;
private transient Set<CompetitorChangeListener> listeners;
public CompetitorImpl(Serializable id, String name, Color color, DynamicTeam team, DynamicBoat boat) {
this.id = id;
@@ -21,6 +27,12 @@ public class CompetitorImpl implements DynamicCompetitor {
this.team = team;
this.boat = boat;
this.color = color;
this.listeners = new HashSet<CompetitorChangeListener>();
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
listeners = new HashSet<CompetitorChangeListener>();
}
@Override
@@ -35,7 +47,13 @@ public class CompetitorImpl implements DynamicCompetitor {
@Override
public void setName(String newName) {
this.name = newName;
final String oldName = this.name;
if (!Util.equalsWithNull(oldName, newName)) {
this.name = newName;
for (CompetitorChangeListener listener : getListeners()) {
listener.nameChanged(oldName, newName);
}
}
}
@Override
@@ -64,6 +82,36 @@ public class CompetitorImpl implements DynamicCompetitor {
}
public void setColor(Color color) {
this.color = color;
final Color oldColor = this.color;
if (!Util.equalsWithNull(oldColor, color)) {
this.color = color;
for (CompetitorChangeListener listener : getListeners()) {
listener.colorChanged(oldColor, color);
}
}
}
@Override
public void addCompetitorChangeListener(CompetitorChangeListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
getBoat().addBoatChangeListener(listener);
getTeam().addNationalityChangeListener(listener);
}
@Override
public void removeCompetitorChangeListener(CompetitorChangeListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
getBoat().removeCompetitorChangeListener(listener);
getTeam().removeNationalityChangeListener(listener);
}
private Iterable<CompetitorChangeListener> getListeners() {
synchronized (listeners) {
return new HashSet<CompetitorChangeListener>(listeners);
}
}
}
@@ -1,7 +1,13 @@
package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.base.Boat;
import com.sap.sailing.domain.base.BoatChangeListener;
public interface DynamicBoat extends Boat {
void setSailId(String newSailId);
void addBoatChangeListener(BoatChangeListener listener);
void removeCompetitorChangeListener(BoatChangeListener listener);
}
@@ -1,8 +1,13 @@
package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.NationalityChangeListener;
import com.sap.sailing.domain.base.Person;
public interface DynamicPerson extends Person {
void setNationality(Nationality newNationality);
void addNationalityChangeListener(NationalityChangeListener listener);
void removeNationalityChangeListener(NationalityChangeListener listener);
}
@@ -1,8 +1,13 @@
package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.NationalityChangeListener;
import com.sap.sailing.domain.base.Team;
public interface DynamicTeam extends Team {
void setNationality(Nationality newNationality);
void addNationalityChangeListener(NationalityChangeListener listener);
void removeNationalityChangeListener(NationalityChangeListener listener);
}
@@ -1,24 +1,37 @@
package com.sap.sailing.domain.base.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.NationalityChangeListener;
import com.sap.sailing.domain.common.impl.NamedImpl;
import com.sap.sailing.domain.common.impl.Util;
public class PersonImpl extends NamedImpl implements DynamicPerson {
private static final long serialVersionUID = -2104903799224233508L;
private Nationality nationality;
private final Date dateOfBirth;
private final String description;
private transient Set<NationalityChangeListener> listeners;
public PersonImpl(String name, Nationality nationality, Date dateOfBirth, String description) {
super(name);
this.nationality = nationality;
this.dateOfBirth = dateOfBirth;
this.description = description;
this.listeners = new HashSet<NationalityChangeListener>();
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
listeners = new HashSet<NationalityChangeListener>();
}
@Override
public InputStream getImage() {
throw new UnsupportedOperationException("Image for person not yet implemented");
@@ -31,7 +44,13 @@ public class PersonImpl extends NamedImpl implements DynamicPerson {
@Override
public void setNationality(Nationality newNationality) {
this.nationality = newNationality;
final Nationality oldNationality = this.nationality;
if (!Util.equalsWithNull(oldNationality, newNationality)) {
this.nationality = newNationality;
for (NationalityChangeListener listener : getListeners()) {
listener.nationalityChanged(this, oldNationality, newNationality);
}
}
}
@Override
@@ -44,4 +63,23 @@ public class PersonImpl extends NamedImpl implements DynamicPerson {
return description;
}
@Override
public void addNationalityChangeListener(NationalityChangeListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
@Override
public void removeNationalityChangeListener(NationalityChangeListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
private Iterable<NationalityChangeListener> getListeners() {
synchronized (listeners) {
return new HashSet<NationalityChangeListener>(listeners);
}
}
}
@@ -1,18 +1,80 @@
package com.sap.sailing.domain.base.impl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.HashSet;
import java.util.Set;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.NationalityChangeListener;
import com.sap.sailing.domain.base.Person;
import com.sap.sailing.domain.base.WithNationality;
import com.sap.sailing.domain.common.impl.NamedImpl;
import com.sap.sailing.domain.common.impl.Util;
public class TeamImpl extends NamedImpl implements DynamicTeam {
private static final long serialVersionUID = 4646922280429210183L;
private final Iterable<? extends DynamicPerson> sailors;
private final Person coach;
private final DynamicPerson coach;
private final NationalityChangeListener personNationalityChangeForwarder;
private transient Set<NationalityChangeListener> listeners;
public TeamImpl(String name, Iterable<? extends DynamicPerson> sailors, Person coach) {
public TeamImpl(String name, Iterable<? extends DynamicPerson> sailors, DynamicPerson coach) {
super(name);
listeners = new HashSet<NationalityChangeListener>();
this.sailors = sailors;
this.coach = coach;
this.personNationalityChangeForwarder = new NationalityChangeListener() {
private static final long serialVersionUID = -6024941913105410444L;
@Override
public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) {
if (what == getNationalityDonor()) {
for (NationalityChangeListener listener : getNationalityChangeListeners()) {
listener.nationalityChanged(TeamImpl.this, oldNationality, newNationality);
}
}
}
};
setTransitiveListeners();
}
/**
* The listeners on {@link DynamicPerson} are transient. Our {@link #personNationalityChangeForwarder} needs to
* be restored after deserialization.
*/
private void setTransitiveListeners() {
if (this.sailors != null) {
for (DynamicPerson sailor : this.sailors) {
sailor.addNationalityChangeListener(personNationalityChangeForwarder);
}
}
if (this.coach != null) {
this.coach.addNationalityChangeListener(personNationalityChangeForwarder);
}
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
listeners = new HashSet<NationalityChangeListener>();
setTransitiveListeners();
}
public void addNationalityChangeListener(NationalityChangeListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
public void removeNationalityChangeListener(NationalityChangeListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
private Iterable<NationalityChangeListener> getNationalityChangeListeners() {
synchronized (listeners) {
return new HashSet<NationalityChangeListener>(listeners);
}
}
@Override
@@ -21,28 +83,38 @@ public class TeamImpl extends NamedImpl implements DynamicTeam {
}
@Override
public Person getCoach() {
public DynamicPerson getCoach() {
return coach;
}
@Override
public Nationality getNationality() {
return getNationalityDonor().getNationality();
}
private WithNationality getNationalityDonor() {
for (Person sailor : getSailors()) {
if (sailor.getNationality() != null) {
return sailor.getNationality();
return sailor;
}
}
if (getCoach() != null) {
return getCoach().getNationality();
return getCoach();
}
return null;
}
@Override
public void setNationality(Nationality newNationality) {
for (Person sailor : getSailors()) {
((DynamicPerson) sailor).setNationality(newNationality);
Nationality oldNationality = getNationality();
if (!Util.equalsWithNull(oldNationality, newNationality)) {
for (Person sailor : getSailors()) {
((DynamicPerson) sailor).setNationality(newNationality);
}
for (NationalityChangeListener listener : getNationalityChangeListeners()) {
listener.nationalityChanged(this, oldNationality, newNationality);
}
}
}
}
@@ -299,12 +299,19 @@ public class TrackImpl<FixType extends Timed> implements Track<FixType> {
protected boolean add(FixType fix) {
lockForWrite();
try {
return getInternalRawFixes().add(fix);
return addWithoutLocking(fix);
} finally {
unlockAfterWrite();
}
}
/**
* The caller must ensure to hold the write lock for this track when calling this methos
*/
protected boolean addWithoutLocking(FixType fix) {
return getInternalRawFixes().add(fix);
}
@Override
public Duration getAverageIntervalBetweenFixes() {
lockForRead();
@@ -0,0 +1,243 @@
package com.sap.sailing.util.impl;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.sap.sailing.domain.common.impl.Util;
/**
* A map that is concurrency-safe as provided by the {@link ConcurrentHashMap} implementation, and referencing
* keys only weakly. Like {@link ConcurrentHashMap}, this map does not support <code>null</code> keys.
*
* @author Axel Uhl (D043530)
*/
public class ConcurrentWeakHashMap<K, V> implements Map<K, V> {
private final ConcurrentHashMap<WeakReferenceThatIsEqualToItselfAndItsReferent, V> map;
private final ReferenceQueue<K> queue;
/**
* A reference whose hash code is based on the hash code of the original referent, also after the
* reference has been enqueued. Equality is defined such that this reference is equal to itself which is
* helpful to remove the entries from the {@link ConcurrentHashMap}, as well as equal to any other reference
* of this type if their referents are equal.
*
* @author Axel Uhl (D043530)
*
*/
private class WeakReferenceThatIsEqualToItselfAndItsReferent extends WeakReference<K> {
private final int hashCode;
public WeakReferenceThatIsEqualToItselfAndItsReferent(K referent, ReferenceQueue<? super K> q) {
super(referent, q);
hashCode = referent.hashCode();
}
@Override
public int hashCode() {
return hashCode;
}
/**
* If <code>other</code> is this reference, it it considered equal. Otherwise, <code>other</code> is considered
* equal to this reference if the referent is still set and the referent equals <code>other</code>.
*/
@Override
public boolean equals(Object other) {
final boolean result;
if (other == this) {
result = true;
} else {
@SuppressWarnings("unchecked") // don't care about the generics here; important is that we can get at the referent
WeakReferenceThatIsEqualToItselfAndItsReferent otherAsRef = (WeakReferenceThatIsEqualToItselfAndItsReferent) other;
K referent = get();
K otherReferent = otherAsRef.get();
result = Util.equalsWithNull(referent, otherReferent);
}
return result;
}
}
private class KeySet extends AbstractSet<K> {
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean contains(Object o) {
return map.containsKey(o);
}
@Override
public boolean remove(Object o) {
return map.remove(o) != null;
}
@Override
public Iterator<K> iterator() {
purgeStaleEntries();
final Iterator<Map.Entry<WeakReferenceThatIsEqualToItselfAndItsReferent, V>> iterator = map.entrySet().iterator();
return new Iterator<K>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public K next() {
return iterator.next().getKey().get();
}
@Override
public void remove() {
iterator.remove();
}
};
}
}
private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public Iterator<Map.Entry<K, V>> iterator() {
purgeStaleEntries();
final Iterator<Map.Entry<WeakReferenceThatIsEqualToItselfAndItsReferent, V>> iterator = map.entrySet().iterator();
return new Iterator<Map.Entry<K, V>>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Map.Entry<K, V> next() {
final Map.Entry<WeakReferenceThatIsEqualToItselfAndItsReferent, V> e = iterator.next();
return new Map.Entry<K, V>() {
@Override
public K getKey() {
return e.getKey().get();
}
@Override
public V getValue() {
return e.getValue();
}
@Override
public V setValue(V value) {
return e.setValue(value);
}
};
}
@Override
public void remove() {
iterator.remove();
}
};
}
}
public ConcurrentWeakHashMap() {
map = new ConcurrentHashMap<WeakReferenceThatIsEqualToItselfAndItsReferent, V>();
queue = new ReferenceQueue<K>();
}
private void purgeStaleEntries() {
Reference<? extends K> ref;
while ((ref=queue.poll()) != null) {
map.remove(ref);
}
}
@Override
public int size() {
purgeStaleEntries();
return map.size();
}
@Override
public boolean isEmpty() {
purgeStaleEntries();
return map.isEmpty();
}
@SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so false will result which is ok
@Override
public boolean containsKey(Object key) {
purgeStaleEntries();
return map.containsKey(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null));
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so null will result which is ok
@Override
public V get(Object key) {
purgeStaleEntries();
return map.get(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null));
}
@Override
public V put(K key, V value) {
return map.put(new WeakReferenceThatIsEqualToItselfAndItsReferent(key, queue), value);
}
@SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so null will result which is ok
@Override
public V remove(Object key) {
return map.remove(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null));
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<K> keySet() {
return new KeySet();
}
@Override
public Collection<V> values() {
return map.values();
}
@Override
public Set<java.util.Map.Entry<K, V>> entrySet() {
return new EntrySet();
}
}
@@ -1,9 +1,10 @@
package com.sap.sailing.util.impl;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
@@ -51,7 +52,7 @@ public class LockUtil {
private static final int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5;
private static final Logger logger = Logger.getLogger(Util.class.getName());
private static final Map<NamedReentrantReadWriteLock, TimePoint> lastTimeWriteLockWasObtained = new WeakHashMap<NamedReentrantReadWriteLock, TimePoint>();
private static final Map<NamedReentrantReadWriteLock, TimePoint> lastTimeWriteLockWasObtained = new ConcurrentWeakHashMap<NamedReentrantReadWriteLock, TimePoint>();
/**
* Tells how many other threads propagated which held lock to the key thread. During propagation, a lock is
@@ -63,20 +64,20 @@ public class LockUtil {
* The thread-specific value maps are used as monitor objects whenever decisions about thread-specific lock counts
* need to be made.
*/
private static final Map<Thread, Map<Lock, Integer>> propagationCounts = new ConcurrentHashMap<Thread, Map<Lock,Integer>>();
private static final Map<Thread, Map<Lock, Integer>> propagationCounts = new ConcurrentWeakHashMap<Thread, Map<Lock,Integer>>();
/**
* Counts the "virtual" locks. A "virtual" lock is obtained if and only if at the time of calling
* {@link #lockForRead(NamedReentrantReadWriteLock)} or {@link #lockForWrite(NamedReentrantReadWriteLock)} the
* respective lock has a positive {@link #propagationCounts propagation count} for the current thread.
*/
private static final Map<Thread, Map<Lock, Integer>> virtualLockCounts = new ConcurrentHashMap<Thread, Map<Lock, Integer>>();
private static final Map<Thread, Map<Lock, Integer>> virtualLockCounts = new ConcurrentWeakHashMap<Thread, Map<Lock, Integer>>();
/**
* Redundant but easily accessible hold count per thread and lock. These are the actual lock hold counts as they are
* recorded in the actual {@link NamedReentrantReadWriteLock} locks.
*/
private static final Map<Thread, Map<Lock, Integer>> lockCounts = new ConcurrentHashMap<Thread, Map<Lock, Integer>>();
private static final Map<Thread, Map<Lock, Integer>> lockCounts = new ConcurrentWeakHashMap<Thread, Map<Lock, Integer>>();
public static void lockForRead(NamedReentrantReadWriteLock lock) {
acquireLockVirtuallyOrActually(lock, lock.readLock(), ReadOrWrite.READ);
@@ -84,9 +85,7 @@ public class LockUtil {
public static void lockForWrite(NamedReentrantReadWriteLock lock) {
acquireLockVirtuallyOrActually(lock, lock.writeLock(), ReadOrWrite.WRITE);
synchronized (lastTimeWriteLockWasObtained) {
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
}
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
}
private static void acquireLockVirtuallyOrActually(NamedReentrantReadWriteLock lock, final Lock readOrWriteLock, final ReadOrWrite readOrWrite) {
@@ -147,9 +146,7 @@ public class LockUtil {
unlockVirtuallyOrActually(lock, lock.writeLock());
}
final TimePoint timePointWriteLockWasObtained;
synchronized (lastTimeWriteLockWasObtained) {
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
}
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
if (timePointWriteLockWasObtained == null) {
logger.info("Internal error: write lock " + lock.getName()
+ " to be unlocked but no time recorded for when it was last obtained.\n"
@@ -355,9 +352,9 @@ public class LockUtil {
}
private static String getStackTrace(Thread thread) {
private static String formatStackTrace(StackTraceElement[] stackTrace) {
StringBuilder sb = new StringBuilder();
for (StackTraceElement sf : thread.getStackTrace()) {
for (StackTraceElement sf : stackTrace) {
sb.append(sf.toString());
sb.append('\n');
}
@@ -365,7 +362,7 @@ public class LockUtil {
}
private static String getCurrentStackTrace() {
return getStackTrace(Thread.currentThread());
return formatStackTrace(Thread.currentThread().getStackTrace());
}
/**
@@ -381,6 +378,14 @@ public class LockUtil {
try {
locked = lock.tryLock(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK, TimeUnit.SECONDS);
if (!locked) {
Thread writer = lockParent.getWriter();
// capture the stack traces as quickly as possible to try to reflect the situation as it was when the lock couuldn't be obtained
StackTraceElement[] writerStackTrace = writer != null ? writer.getStackTrace() : null;
Map<Thread, StackTraceElement[]> readerStackTraces = new HashMap<Thread, StackTraceElement[]>();
final List<Thread> readers = lockParent.getReaders();
for (Thread reader : readers) {
readerStackTraces.put(reader, reader.getStackTrace());
}
StringBuilder message = new StringBuilder();
message.append("Couldn't acquire lock ");
message.append(lockDescriptionForTimeoutLogMessage);
@@ -388,14 +393,15 @@ public class LockUtil {
message.append(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK);
message.append("s in thread " + Thread.currentThread().getName() + " at ");
message.append(getCurrentStackTrace());
Thread writer = lockParent.getWriter();
if (writer != null) {
message.append("\nThe current writer is:\n");
appendThreadData(message, writer);
appendThreadData(message, writer, writerStackTrace);
}
message.append("\nThe current readers are:\n");
for (Thread reader : lockParent.getReaders()) {
appendThreadData(message, reader);
if (readers != null && !readers.isEmpty()) {
message.append("\nThe current readers are:\n");
for (Thread reader : readers) {
appendThreadData(message, reader, readerStackTraces.get(reader));
}
}
message.append("Trying again...");
logger.info(message.toString());
@@ -406,10 +412,10 @@ public class LockUtil {
return locked;
}
private static void appendThreadData(StringBuilder message, Thread writer) {
private static void appendThreadData(StringBuilder message, Thread writer, StackTraceElement[] stackTrace) {
message.append(writer);
message.append('\n');
message.append(getStackTrace(writer));
message.append(formatStackTrace(stackTrace));
message.append('\n');
}
@@ -0,0 +1,197 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.base.CompetitorChangeListener;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.Team;
import com.sap.sailing.domain.base.WithNationality;
import com.sap.sailing.domain.base.impl.DomainFactoryImpl;
import com.sap.sailing.domain.base.impl.DynamicCompetitor;
import com.sap.sailing.domain.common.Color;
import com.sap.sailing.domain.common.impl.RGBColor;
/**
* Tests the {@link CompetitorChangeListener} stuff including the serialization of the transitive listener
* pattern on the {@link Team} structure.
*
* @author Axel Uhl (D043530)
*
*/
public class CompetitorListenerTest extends AbstractSerializationTest {
private CompetitorChangeListener listener;
private boolean nationalityChanged;
private Object whatChangedNationality;
private Object oldNationality;
private Object newNationality;
private boolean sailIdChanged;
private String oldSailId;
private String newSailId;
private boolean nameChanged;
private String oldName;
private String newName;
private boolean colorChanged;
private Color oldColor;
private Color newColor;
private DynamicCompetitor competitor;
private DomainFactoryImpl baseDomainFactory;
@Before
public void setUp() {
baseDomainFactory = new DomainFactoryImpl();
competitor = TrackBasedTest.createCompetitor("Hasso");
nationalityChanged = false;
whatChangedNationality = null;
oldNationality = null;
newNationality = null;
sailIdChanged = false;
oldSailId = null;
newSailId = null;
nameChanged = false;
oldName = null;
newName = null;
colorChanged = false;
oldColor = null;
newColor = null;
listener = new CompetitorChangeListener() {
private static final long serialVersionUID = 4581029778988240209L;
@Override
public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) {
nationalityChanged = true;
whatChangedNationality = what;
CompetitorListenerTest.this.oldNationality = oldNationality;
CompetitorListenerTest.this.newNationality = newNationality;
}
@Override
public void sailIdChanged(String oldSailId, String newSailId) {
sailIdChanged = true;
CompetitorListenerTest.this.oldSailId = oldSailId;
CompetitorListenerTest.this.newSailId = newSailId;
}
@Override
public void nameChanged(String oldName, String newName) {
nameChanged = true;
CompetitorListenerTest.this.oldName = oldName;
CompetitorListenerTest.this.newName = newName;
}
@Override
public void colorChanged(Color oldColor, Color newColor) {
colorChanged = true;
CompetitorListenerTest.this.oldColor = oldColor;
CompetitorListenerTest.this.newColor = newColor;
}
};
competitor.addCompetitorChangeListener(listener);
}
@Test
public void testSimpleCompetitorListenerPatternForName() {
competitor.setName("Dr. Hasso Plattner");
assertTrue(nameChanged);
assertEquals("Hasso", oldName);
assertEquals("Dr. Hasso Plattner", newName);
assertFalse(sailIdChanged);
assertFalse(colorChanged);
assertFalse(nationalityChanged);
}
@Test
public void testSimpleCompetitorListenerPatternForSailId() {
final String myOldSailId = competitor.getBoat().getSailID();
competitor.getBoat().setSailId("POR 40");
assertFalse(nameChanged);
assertTrue(sailIdChanged);
assertEquals(myOldSailId, oldSailId);
assertEquals("POR 40", newSailId);
assertFalse(colorChanged);
assertFalse(nationalityChanged);
}
@Test
public void testSimpleCompetitorListenerPatternForColor() {
final Color myOldColor = competitor.getColor();
final RGBColor myNewColor = new RGBColor(123, 12, 234);
competitor.setColor(myNewColor);
assertFalse(nameChanged);
assertFalse(sailIdChanged);
assertTrue(colorChanged);
assertEquals(myOldColor, oldColor);
assertEquals(myNewColor, newColor);
assertFalse(nationalityChanged);
}
@Test
public void testSimpleCompetitorListenerPatternForNationality() {
final Nationality myOldNationality = competitor.getTeam().getNationality();
final Nationality myNewNationality = DomainFactory.INSTANCE.getOrCreateNationality("POR");
competitor.getTeam().setNationality(myNewNationality);
assertFalse(nameChanged);
assertFalse(sailIdChanged);
assertFalse(colorChanged);
assertTrue(nationalityChanged);
assertEquals(myOldNationality, oldNationality);
assertEquals(myNewNationality, newNationality);
assertSame(competitor.getTeam(), whatChangedNationality);
}
@Test
public void testCompetitorSerializationLosesAllExternalListeners() throws ClassNotFoundException, IOException {
DomainFactory baseDomainFactory = new DomainFactoryImpl();
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
clonedCompetitor.setName("Dr. Hasso Plattner");
clonedCompetitor.getBoat().setSailId("POR 40");
final RGBColor myNewColor = new RGBColor(123, 12, 234);
clonedCompetitor.setColor(myNewColor);
final Nationality myNewNationality = baseDomainFactory.getOrCreateNationality("POR");
clonedCompetitor.getTeam().setNationality(myNewNationality);
assertFalse(nameChanged);
assertFalse(sailIdChanged);
assertFalse(colorChanged);
assertFalse(nationalityChanged);
}
@Test
public void testCompetitorSerializationListenerPatternForNameOnSerializedCompetitor() throws ClassNotFoundException, IOException {
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
competitor = clonedCompetitor;
competitor.addCompetitorChangeListener(listener);
testSimpleCompetitorListenerPatternForName();
}
@Test
public void testCompetitorSerializationListenerPatternForColorOnSerializedCompetitor() throws ClassNotFoundException, IOException {
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
competitor = clonedCompetitor;
competitor.addCompetitorChangeListener(listener);
testSimpleCompetitorListenerPatternForColor();
}
@Test
public void testCompetitorSerializationListenerPatternForSailIdOnSerializedCompetitor() throws ClassNotFoundException, IOException {
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
competitor = clonedCompetitor;
competitor.addCompetitorChangeListener(listener);
testSimpleCompetitorListenerPatternForSailId();
}
@Test
public void testCompetitorSerializationListenerPatternForNationalityOnSerializedCompetitor() throws ClassNotFoundException, IOException {
DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory);
competitor = clonedCompetitor;
competitor.addCompetitorChangeListener(listener);
testSimpleCompetitorListenerPatternForNationality();
}
}
@@ -0,0 +1,133 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import org.junit.Test;
import com.sap.sailing.util.impl.ConcurrentWeakHashMap;
/**
* Tests the class {@link ConcurrentWeakHashMap}.
*
* @author Axel Uhl (D043530)
*
*/
public class ConcurrentWeakHashMapTest {
@Test
public void testPutAndGet() {
ConcurrentWeakHashMap<String, String> m = new ConcurrentWeakHashMap<>();
m.put("a", "b");
assertEquals("b", m.get("a"));
}
@Test
public void testPutAndIterate() {
ConcurrentWeakHashMap<String, String> m = new ConcurrentWeakHashMap<>();
m.put("a", "b");
assertEquals("b", m.values().iterator().next());
assertEquals("a", m.keySet().iterator().next());
assertEquals("a", m.entrySet().iterator().next().getKey());
}
@Test
public void testPutAndPutAgain() {
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
Integer one1 = new Integer(1);
Integer one2 = new Integer(1);
assertNotSame(one1, one2);
m.put(one1, "a");
String replaced = m.put(one2, "b");
assertEquals("a", replaced);
assertEquals(1, m.size());
assertEquals(m.keySet().iterator().next(), one2);
}
@Test
public void testRemoveWithSame() {
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
Integer one = new Integer(1);
m.put(one, "a");
m.remove(one);
assertTrue(m.isEmpty());
}
@Test
public void testRemoveImpactOnKeySetAndEntrySet() {
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
Integer one = new Integer(1);
m.put(one, "a");
Set<Integer> keySet = m.keySet();
Set<Entry<Integer, String>> entrySet = m.entrySet();
assertEquals(1, keySet.size());
assertEquals(1, entrySet.size());
m.remove(one);
assertTrue(m.isEmpty());
assertTrue(keySet.isEmpty());
assertTrue(entrySet.isEmpty());
}
@Test
public void testRemoveWithEqual() {
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
Integer one1 = new Integer(1);
Integer one2 = new Integer(1);
assertNotSame(one1, one2);
m.put(one1, "a");
m.remove(one2);
assertTrue(m.isEmpty());
}
@Test
public void removeThrougKeySet() {
ConcurrentWeakHashMap<Integer, String> m = new ConcurrentWeakHashMap<>();
Integer one1 = new Integer(1);
m.put(one1, "a");
final Set<Integer> keySet = m.keySet();
Iterator<Integer> keySetIter = keySet.iterator();
keySetIter.next();
keySetIter.remove();
assertTrue(m.isEmpty());
assertTrue(keySet.isEmpty());
assertTrue(m.entrySet().isEmpty());
}
@Test
public void testPutAndGetWithSubsequentGC() {
Object key = createKeyObject();
ConcurrentWeakHashMap<Object, String> m = new ConcurrentWeakHashMap<>();
m.put(key, "b");
assertEquals("b", m.get(key));
key = null;
forceEnqueuableWeakReferencesToBeEnqueued();
assertTrue(m.isEmpty());
}
private void forceEnqueuableWeakReferencesToBeEnqueued() {
System.gc();
try {
long[] hugeArrayThatWontGetAllocated = new long[Integer.MAX_VALUE];
assertTrue(hugeArrayThatWontGetAllocated.length > 0);
} catch (OutOfMemoryError e) {
// Ignore OME
}
try {
Thread.sleep(50); // give GC some time to finish enqueuing and finalizing all weakly-referenced objects
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* Use a separate method for this, assuming that the compiler won't put the object on the using method's stack
* @return
*/
private Object createKeyObject() {
return new Object();
}
}
@@ -0,0 +1,182 @@
package com.sap.sailing.domain.test;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.WindWithConfidence;
import com.sap.sailing.domain.tracking.impl.TrackBasedEstimationWindTrackImpl;
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
import com.sap.sailing.domain.tracking.impl.WindImpl;
import com.sap.sailing.domain.tracking.impl.WindTrackImpl;
import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
/**
* See the issues documented in bug 1923 (http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=1923).
* This test mocks {@link TrackedRace} to the degree necessary and fires more or less random "measured"
* wind fixes against the {@link TrackBasedEstimationWindTrackImpl} while concurrently reading massively
* from it. With this, the test is supposed to reproduce conditions as observed during events, leading
* up to bug 1923. This can then be the basis for a fix.
*
* @author Axel Uhl (D043530)
*
*/
public class WindEstimationLockingUnderLoadTest {
private static final Logger logger = Logger.getLogger(WindEstimationLockingUnderLoadTest.class.getName());
private static final int MIN_WIND_SPEED_IN_KNOTS = 5;
private static final int MAX_WIND_SPEED_IN_KNOTS = 20;
private TrackedRace mockedTrackedRace;
private WindSource realWindSource;
private TrackBasedEstimationWindTrackImpl estimationTrack;
private WindTrackImpl measuredTrack;
@Before
public void setUp() {
realWindSource = new WindSourceWithAdditionalID(WindSourceType.EXPEDITION, "1");
measuredTrack = new WindTrackImpl(WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, /* useSpeed */ true, /* nameForReadWriteLock */ "Test wind track in "+getClass().getName());
mockedTrackedRace = mockTrackedRace();
estimationTrack = new TrackBasedEstimationWindTrackImpl(mockedTrackedRace, WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, 0.5);
}
@After
public void tearDown() {
// clean up all Mockito stubbing leaks, particularly the InvocationImpl objects attached to any ThreadLocal;
// see also bug 1923, comment #9.
Mockito.reset();
}
private TrackedRace mockTrackedRace() {
TrackedRace result = mock(TrackedRace.class);
RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class);
when(result.getRace()).thenReturn(mockedRaceDefinition);
when(mockedRaceDefinition.getName()).thenReturn("Test Race");
when(result.getEstimatedWindDirectionWithConfidence((Position) any(), (TimePoint) any())).thenAnswer(new Answer<WindWithConfidence<TimePoint>>() {
public WindWithConfidence<TimePoint> answer(InvocationOnMock invocation) {
return randomWindOrNull();
}
});
when(result.getOrCreateWindTrack(realWindSource)).thenReturn(measuredTrack);
when(result.getStatus()).thenReturn(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, /* loadingProgress */ 1.0));
when(result.getMillisecondsOverWhichToAverageWind()).thenReturn(30000l);
return result;
}
private WindWithConfidence<TimePoint> randomWindOrNull() {
final WindWithConfidence<TimePoint> result;
double speedInKnots = MAX_WIND_SPEED_IN_KNOTS*Math.random();
if (speedInKnots < MIN_WIND_SPEED_IN_KNOTS) {
result = null;
} else {
double directionInDegrees = 360*Math.random();
double confidence = Math.random();
final MillisecondsTimePoint now = MillisecondsTimePoint.now();
result = new WindWithConfidenceImpl<TimePoint>(new WindImpl(new DegreePosition(49, 8), now, new KnotSpeedWithBearingImpl(speedInKnots, new DegreeBearingImpl(directionInDegrees))),
confidence, now, /* useSpeed */ true);
}
return result;
}
@Test
public void testSimpleWindEstimation() {
// We can expect MIN_WIND_SPEED_IN_KNOTS/MAX_WIND_SPEED_IN_KNOTS results to be null, but as there is a significant
// difference between the two, with a sufficient amount of random samples there has to be at least one non-null
// result
int notNullCount = 0;
for (int i=0; i<1000; i++) {
Wind estimatedWind = estimationTrack.getAveragedWind(/* position */ null, MillisecondsTimePoint.now().minus(10000*i));
if (estimatedWind != null) {
notNullCount++;
}
}
assertTrue(notNullCount > 0);
}
@Test
public void testSendingNewWindFixToEstimationTrack() {
final WindImpl wind = new WindImpl(new DegreePosition(49, 8), MillisecondsTimePoint.now(),
new KnotSpeedWithBearingImpl(/* speedInKnots */12., new DegreeBearingImpl(47)));
addMeasuredWindFix(wind);
// no verdict, really; just expecting that there is no exception raised
}
@Test
public void testAddingManyWindFixesWhileReadingLikeCrazy() throws InterruptedException {
Thread inserter = new Thread("Inserter thread in "+getClass().getName()+".testAddingManyWindFixesWhileReadingLikeCrazy") {
@Override
public void run() {
TimePoint now = MillisecondsTimePoint.now();
for (int i = 0; i < 100000; i++) {
final WindImpl wind = new WindImpl(new DegreePosition(49, 8), now, new KnotSpeedWithBearingImpl(
/* speedInKnots */12., new DegreeBearingImpl(47)));
addMeasuredWindFix(wind);
now = now.plus(10);
}
logger.info("Inserter thread done");
}
};
Runnable readerRunnable = new Runnable() {
@Override
public void run() {
TimePoint now = MillisecondsTimePoint.now();
for (int i=0; i<100000; i++) {
estimationTrack.getAveragedWind(/* position */ null, now);
now = now.plus(10); // the cache quantizes to a full second; so not all time points will automatically fetch uncached values
}
logger.info(Thread.currentThread().getName()+" done");
}
};
inserter.start();
List<Thread> readers = new ArrayList<Thread>();
for (int i = 0; i < 4; i++) {
Thread reader = new Thread(readerRunnable, "Reader thread "+i+" in " + getClass().getName()
+ ".testAddingManyWindFixesWhileReadingLikeCrazy");
reader.start();
readers.add(reader);
}
inserter.join();
for (Thread reader : readers) {
reader.join();
}
}
/**
* Adds the wind fix to the {@link #measuredTrack} and explicitly notifies {@link #estimationTrack} which is
* not a listener on the mocked tracked race or the wind track and therefore otherwise wouldn't receive the fix
*/
private void addMeasuredWindFix(final WindImpl wind) {
measuredTrack.add(wind);
estimationTrack.windDataReceived(wind, realWindSource);
}
}
@@ -35,6 +35,7 @@ import com.sap.sailing.domain.leaderboard.impl.HighPointExtremeSailingSeriesOver
import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets10LastBreaksTie;
import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets1LastBreaksTie;
import com.sap.sailing.domain.leaderboard.impl.HighPointLastBreaksTie;
import com.sap.sailing.domain.leaderboard.impl.HighPointWinnerGetsSix;
import com.sap.sailing.domain.leaderboard.impl.LowPoint;
import com.sap.sailing.domain.leaderboard.impl.LowPointWinnerGetsZero;
import com.sap.sailing.domain.tracking.GPSFix;
@@ -86,6 +87,8 @@ public class DomainFactoryImpl extends SharedDomainFactoryImpl implements Domain
return new HighPointFirstGets10LastBreaksTie();
case LOW_POINT_WINNER_GETS_ZERO:
return new LowPointWinnerGetsZero();
case HIGH_POINT_WINNER_GETS_SIX:
return new HighPointWinnerGetsSix();
}
throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name());
}
@@ -7,11 +7,15 @@ import java.util.Set;
import java.util.WeakHashMap;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CompetitorChangeListener;
import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.base.RaceColumnListener;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.WithNationality;
import com.sap.sailing.domain.common.Color;
import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
@@ -39,7 +43,8 @@ import com.sap.sailing.util.impl.NamedReentrantReadWriteLock;
*/
public class LeaderboardCacheManager {
private final WeakHashMap<Leaderboard, CacheInvalidationUponScoreCorrectionListener> scoreCorrectionListeners;
private final NamedReentrantReadWriteLock scoreCorrectionListenersLock;
private final NamedReentrantReadWriteLock scoreCorrectionAndCompetitorChangeListenersLock;
private final WeakHashMap<Leaderboard, CacheInvalidationUponCompetitorChangeListener> competitorChangeListeners;
private final WeakHashMap<Leaderboard, Map<TrackedRace, Set<CacheInvalidationListener>>> invalidationListenersPerLeaderboard;
private final WeakHashMap<Leaderboard, RaceColumnListener> raceColumnListeners;
@@ -113,6 +118,64 @@ public class LeaderboardCacheManager {
}
}
private class CacheInvalidationUponCompetitorChangeListener implements CompetitorChangeListener {
private static final long serialVersionUID = 8308312509904366143L;
private final Leaderboard leaderboard;
private final Set<Competitor> observedCompetitors;
/**
* Registers this listener as listener for all of the <code>leaderboard</code>'s current
* {@link Leaderboard#getCompetitors() competitors}
*/
public CacheInvalidationUponCompetitorChangeListener(Leaderboard leaderboard) {
this.leaderboard = leaderboard;
observedCompetitors = new HashSet<>();
updateCompetitorListeners();
}
@Override
public void sailIdChanged(String oldSailId, String newSailId) {
removeFromCache(leaderboard);
}
@Override
public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) {
removeFromCache(leaderboard);
}
@Override
public void colorChanged(Color oldColor, Color newColor) {
removeFromCache(leaderboard);
}
@Override
public void nameChanged(String oldName, String newName) {
removeFromCache(leaderboard);
}
public synchronized void updateCompetitorListeners() {
Set<Competitor> competitorsToStopObserving = new HashSet<>(observedCompetitors);
for (Competitor competitor : leaderboard.getCompetitors()) {
if (!observedCompetitors.contains(competitor)) {
competitor.addCompetitorChangeListener(this); // start observing competitor
} else {
competitorsToStopObserving.remove(competitor); // keep observing competitor
}
}
for (Competitor competitorToStopObserving : competitorsToStopObserving) {
competitorToStopObserving.removeCompetitorChangeListener(this);
observedCompetitors.remove(competitorToStopObserving);
}
}
public synchronized void removeFromAllCompetitors() {
for (Competitor competitor : observedCompetitors) {
competitor.removeCompetitorChangeListener(this);
}
}
}
private class CacheInvalidationUponScoreCorrectionListener implements ScoreCorrectionListener {
private final Leaderboard leaderboard;
@@ -146,10 +209,12 @@ public class LeaderboardCacheManager {
public LeaderboardCacheManager(LeaderboardCache leaderboardCache) {
this.leaderboardCache = leaderboardCache;
this.invalidationListenersPerLeaderboard = new WeakHashMap<Leaderboard, Map<TrackedRace, Set<CacheInvalidationListener>>>();
this.invalidationListenersPerLeaderboard = new WeakHashMap<>();
this.raceColumnListeners = new WeakHashMap<Leaderboard, RaceColumnListener>();
this.scoreCorrectionListeners = new WeakHashMap<Leaderboard, CacheInvalidationUponScoreCorrectionListener>();
this.scoreCorrectionListenersLock = new NamedReentrantReadWriteLock("Score correction listeners", /* fair */ false);
this.scoreCorrectionListeners = new WeakHashMap<>();
this.scoreCorrectionAndCompetitorChangeListenersLock = new NamedReentrantReadWriteLock(
"Score correction and competitor change listeners", /* fair */false);
this.competitorChangeListeners = new WeakHashMap<>();
}
private void removeFromCache(Leaderboard leaderboard) {
@@ -167,14 +232,21 @@ public class LeaderboardCacheManager {
synchronized (raceColumnListeners) {
leaderboard.removeRaceColumnListener(raceColumnListeners.remove(leaderboard));
}
LockUtil.lockForWrite(scoreCorrectionListenersLock);
LockUtil.lockForWrite(scoreCorrectionAndCompetitorChangeListenersLock);
final CacheInvalidationUponScoreCorrectionListener removedScoreCorrectionListener;
final CacheInvalidationUponCompetitorChangeListener removedCompetitorChangeListener;
try {
removedScoreCorrectionListener = scoreCorrectionListeners.remove(leaderboard);
removedCompetitorChangeListener = competitorChangeListeners.remove(leaderboard);
} finally {
LockUtil.unlockAfterWrite(scoreCorrectionListenersLock);
LockUtil.unlockAfterWrite(scoreCorrectionAndCompetitorChangeListenersLock);
}
if (removedScoreCorrectionListener != null) {
leaderboard.getScoreCorrection().removeScoreCorrectionListener(removedScoreCorrectionListener);
}
if (removedCompetitorChangeListener != null) {
removedCompetitorChangeListener.removeFromAllCompetitors();
}
leaderboard.getScoreCorrection().removeScoreCorrectionListener(removedScoreCorrectionListener);
// invalidate after the listeners have been removed; this is important because invalidation may trigger a
// re-calculation which then in turn may call add(leaderboard) asynchronously again which may be executed
// before the listener removal happens here. This could lead to a race condition where listeners are
@@ -195,22 +267,25 @@ public class LeaderboardCacheManager {
private void registerAsListener(final Leaderboard leaderboard) {
// only add as listener again if not yet added
final boolean containsKey;
LockUtil.lockForRead(scoreCorrectionListenersLock);
LockUtil.lockForRead(scoreCorrectionAndCompetitorChangeListenersLock);
try {
containsKey = scoreCorrectionListeners.containsKey(leaderboard);
} finally {
LockUtil.unlockAfterRead(scoreCorrectionListenersLock);
LockUtil.unlockAfterRead(scoreCorrectionAndCompetitorChangeListenersLock);
}
if (!containsKey) {
LockUtil.lockForWrite(scoreCorrectionListenersLock);
LockUtil.lockForWrite(scoreCorrectionAndCompetitorChangeListenersLock);
try {
for (TrackedRace trackedRace : leaderboard.getTrackedRaces()) {
registerListener(leaderboard, trackedRace);
}
final CacheInvalidationUponScoreCorrectionListener scoreCorrectionListener = new CacheInvalidationUponScoreCorrectionListener(
leaderboard);
final CacheInvalidationUponCompetitorChangeListener competitorChangeListener = new CacheInvalidationUponCompetitorChangeListener(
leaderboard);
leaderboard.getScoreCorrection().addScoreCorrectionListener(scoreCorrectionListener);
scoreCorrectionListeners.put(leaderboard, scoreCorrectionListener);
competitorChangeListeners.put(leaderboard, competitorChangeListener);
final RaceColumnListener raceColumnListener = new RaceColumnListener() {
private static final long serialVersionUID = 8165124797028386317L;
@@ -312,7 +387,7 @@ public class LeaderboardCacheManager {
raceColumnListeners.put(leaderboard, raceColumnListener);
}
} finally {
LockUtil.unlockAfterWrite(scoreCorrectionListenersLock);
LockUtil.unlockAfterWrite(scoreCorrectionAndCompetitorChangeListenersLock);
}
}
}
@@ -24,11 +24,25 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint {
public HighPointFirstGetsFixedScore(double scoreForRaceWinner) {
this.scoreForRaceWinner = scoreForRaceWinner;
}
protected double getScoreForRaceWinner() {
return scoreForRaceWinner;
}
@Override
public Double getScoreForRank(RaceColumn raceColumn, Competitor competitor, int rank, Callable<Integer> numberOfCompetitorsInRaceFetcher, NumberOfCompetitorsInLeaderboardFetcher numberOfCompetitorsInLeaderboardFetcher) {
final int effectiveRank;
final int effectiveRank = getEffectiveRank(raceColumn, competitor, rank);
final Double result;
if (effectiveRank == 0) {
result = null;
} else {
result = Math.max(1.0, (double) (getScoreForRaceWinner() - effectiveRank + 1));
}
return result;
}
protected int getEffectiveRank(RaceColumn raceColumn, Competitor competitor, int rank) {
final int effectiveRank;
int competitorFleetOrdering;
if (rank == 0) {
effectiveRank = 0;
@@ -41,11 +55,6 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint {
effectiveRank = rank;
}
}
if (effectiveRank == 0) {
result = null;
} else {
result = Math.max(1.0, (double) (scoreForRaceWinner - effectiveRank + 1));
}
return result;
return effectiveRank;
}
}
@@ -0,0 +1,16 @@
package com.sap.sailing.domain.leaderboard.impl;
import com.sap.sailing.domain.common.ScoringSchemeType;
public class HighPointWinnerGetsSix extends HighPointFirstGetsFixedScore {
private static final long serialVersionUID = -4116295702464748719L;
public HighPointWinnerGetsSix() {
super(/* scoreForRaceWinner */ 6);
}
@Override
public ScoringSchemeType getType() {
return ScoringSchemeType.HIGH_POINT_WINNER_GETS_SIX;
}
}
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.tracking.impl;
import com.sap.sailing.domain.base.Boat;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CompetitorChangeListener;
import com.sap.sailing.domain.base.IsManagedBySharedDomainFactory;
import com.sap.sailing.domain.base.SharedDomainFactory;
import com.sap.sailing.domain.base.Team;
@@ -63,6 +64,14 @@ public class DummyMarkPassingWithTimePointOnly implements MarkPassing {
public Color getColor() {
return null;
}
@Override
public void addCompetitorChangeListener(CompetitorChangeListener listener) {
}
@Override
public void removeCompetitorChangeListener(CompetitorChangeListener listener) {
}
};
}
}
@@ -1009,7 +1009,7 @@ public class GPSFixTrackImpl<ItemType, FixType extends GPSFix> extends TrackImpl
final boolean result;
lockForWrite();
try {
result = super.add(fix);
result = addWithoutLocking(fix);
invalidateValidityAndDistanceCaches(fix);
} finally {
unlockAfterWrite();
@@ -82,13 +82,21 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
private final NavigableSet<TimePoint> timePointsWithCachedNullResult;
/**
* Access to this cache is guarded by {@link #cacheLock}.
*/
private final NavigableSet<WindWithConfidence<TimePoint>> cache;
/**
* Access to {@link #cache} and {@link #timePointsWithCachedNullResult} and
* {@link #timePointsWithCachedNullResultFastContains} is guarded by this lock.
*/
private final NamedReentrantReadWriteLock cacheLock;
/**
* Access to {@link #scheduledRefreshInterval} is guarded by this lock. In particular, when a client
* holds this lock the client can be sure that {@link InvalidationInterval#isSet()} implies that a
* cache update is scheduled or already running.
*/
private final NamedReentrantReadWriteLock scheduledRefreshIntervalLock;
private final Weigher<TimePoint> weigher;
/**
@@ -106,7 +114,13 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
private static class InvalidationInterval implements Serializable {
private static final long serialVersionUID = -6406690520919193690L;
/**
* Either both {@link #start} and {@link #end} are <code>null</code>, or both are non-<code>null</code>
*/
private WindWithConfidence<TimePoint> start;
/**
* Either both {@link #start} and {@link #end} are <code>null</code>, or both are non-<code>null</code>
*/
private TimePoint end;
public InvalidationInterval() {
super();
@@ -117,18 +131,38 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
public TimePoint getEnd() {
return end;
}
public synchronized void clear() {
public void clear() {
start = null;
end = null;
}
public synchronized boolean isSet() {
public boolean isSet() {
return start != null && end != null;
}
public synchronized void set(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
/**
* Checks whether the interval starting at {@link #start} and ending at {@link #end} is fully contained in this
* interval. Neither of the two arguments is allowed to be <code>null</code>. The interval is contained in this
* interval if this interval {@link #isSet() is set} and this interval's {@link #getStart() start} is at or
* before <code>start</code> and this interval's {@link #getEnd() end} is at or after <code>end</code>.
*/
public boolean contains(TimePoint start, TimePoint end) {
return isSet() && !getStart().getObject().getTimePoint().after(start) && !end.after(getEnd());
}
/**
* Neither of the two arguments is allowed to be <code>null</code>.
*/
public void set(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
assert startOfInvalidation != null;
assert endOfInvalidation != null;
this.start = startOfInvalidation;
this.end = endOfInvalidation;
}
public synchronized void extend(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
/**
* Neither of the two arguments is allowed to be <code>null</code>.
*/
public void extend(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
assert startOfInvalidation != null;
assert endOfInvalidation != null;
if (startOfInvalidation.getObject().getTimePoint().compareTo(start.getObject().getTimePoint()) < 0) {
this.start = startOfInvalidation;
}
@@ -136,11 +170,54 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
end = endOfInvalidation;
}
}
/**
* Leaves this object unchanged. Returns a new {@link InvalidationInterval} object. If the interval defined by
* <code>start</code> and <code>end</code> does not exceed this interval, the resulting interval will have
* {@link #isSet()}<code>==false</code>. Otherwise, the resulting interval will contain all time ranges from
* <code>start</code> to <code>end</code> (inclusive) that are not in this interval. In particular, if
* <code>start</code>..<code>end</code> exceeds this interval on both ends, the resulting interval will start at
* <code>start</code> and end at <code>end</code>. If this interval has {@link #isSet()}<code>==false</code>,
* the resulting interval will range from <code>start</code> to <code>end</code>.
*
* @param start must not be <code>null</code>
* @param end must not be <code>null</code>
*/
public InvalidationInterval subtract(WindWithConfidence<TimePoint> start, TimePoint end) {
assert start != null;
assert end != null;
final InvalidationInterval result = new InvalidationInterval();
if (!isSet()) {
result.set(start, end);
} else {
final WindWithConfidence<TimePoint> newStart;
final TimePoint startTimePoint = getStart().getObject().getTimePoint();
if (start.getObject().getTimePoint().before(startTimePoint)) {
newStart = start;
} else {
// don't go beyond the end of time, avoiding overflow
newStart = getDummyFixWithConfidence(getEnd().asMillis()==Long.MAX_VALUE?getEnd():getEnd().plus(1));
}
final TimePoint newEnd;
if (end.after(getEnd())) {
newEnd = end;
} else {
// avoid underflow
newEnd = startTimePoint.asMillis()==0?startTimePoint:startTimePoint.minus(1);
}
if (!newStart.getObject().getTimePoint().after(newEnd)) {
result.set(newStart, newEnd);
} // else, the resulting interval remains unset
}
return result;
}
}
/**
* {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it
* and when actually invalidating the cache.
* {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it and
* when actually invalidating the cache. When a querying thread holds at least the read lock of {@link #cacheLock}
* and the refresh interval it {@link InvalidationInterval#isSet() is set}, an invalidation is currently running or
* has been scheduled.
*/
private final InvalidationInterval scheduledRefreshInterval;
@@ -171,8 +248,11 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
return o1.getObject().getTimePoint().compareTo(o2.getObject().getTimePoint());
}
});
cacheLock = new NamedReentrantReadWriteLock(TrackBasedEstimationWindTrackImpl.class.getSimpleName()+" for race "+
trackedRace.getRace().getName(), /* fair */ false);
cacheLock = new NamedReentrantReadWriteLock(TrackBasedEstimationWindTrackImpl.class.getSimpleName()
+ " cacheLock for race " + trackedRace.getRace().getName(), /* fair */false);
scheduledRefreshIntervalLock = new NamedReentrantReadWriteLock(
TrackBasedEstimationWindTrackImpl.class.getSimpleName() + " scheduledRefreshIntervalLock for race "
+ trackedRace.getRace().getName(), /* fair */false);
virtualInternalRawFixes = new EstimatedWindFixesAsNavigableSet(trackedRace);
weigher = ConfidenceFactory.INSTANCE
.createHyperbolicTimeDifferenceWeigher(getMillisecondsOverWhichToAverageWind());
@@ -183,9 +263,11 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
}
/**
* Synchronizes serialization on this object to avoid the cache being updated while being written.
* Uses the {@link #cacheLock} and {@link #scheduledRefreshIntervalLock} to make serialization on this object thread
* safe by avoiding the cache being updated while being written.
*/
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
private void writeObject(ObjectOutputStream s) throws IOException {
LockUtil.lockForRead(scheduledRefreshIntervalLock);
LockUtil.lockForRead(cacheLock);
lockForRead();
try {
@@ -193,6 +275,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
} finally {
unlockAfterRead();
LockUtil.unlockAfterRead(cacheLock);
LockUtil.unlockAfterRead(scheduledRefreshIntervalLock);
}
}
@@ -227,29 +310,22 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
return cache;
}
/**
* The caller must <em>not</em> hold the read lock of {@link #cacheLock} when calling this method (unless the caller
* also holds the corresponding write lock) because this method will acquire the corresponding write lock, and
* upgrading from a read lock to a write lock is not possible.
*/
protected void cache(TimePoint timePoint, WindWithConfidence<TimePoint> fix) {
// can't use lockForWrite() here because caching can happen while holding the read lock, and the lock can't be
// upgraded. But lockForRead() and synchronization will do the job because all invalidations lock the write lock,
// and all contains() checks and get() calls use synchronization too.
LockUtil.lockForWrite(cacheLock);
try {
// synchronization necessary to protect writeObject from ConcurrentModificationException
synchronized (this) {
if (fix == null) {
synchronized (timePointsWithCachedNullResult) {
timePointsWithCachedNullResult.add(timePoint);
}
synchronized (timePointsWithCachedNullResultFastContains) {
timePointsWithCachedNullResultFastContains.add(timePoint);
}
} else {
LockUtil.lockForWrite(cacheLock);
try {
cache.add(fix);
} finally {
LockUtil.unlockAfterWrite(cacheLock);
}
}
if (fix == null) {
timePointsWithCachedNullResult.add(timePoint);
timePointsWithCachedNullResultFastContains.add(timePoint);
} else {
cache.add(fix);
}
} finally {
LockUtil.unlockAfterWrite(cacheLock);
@@ -263,19 +339,29 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
* will not be massively delayed by having to re-calculate the estimation over and over again.
*/
private void scheduleCacheRefresh(WindWithConfidence<TimePoint> startOfInvalidation, TimePoint endOfInvalidation) {
LockUtil.lockForWrite(cacheLock);
final boolean alreadyContained;
LockUtil.lockForRead(scheduledRefreshIntervalLock);
try {
if (!scheduledRefreshInterval.isSet()) {
// according to the invariant this implies [1]==null
scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation);
startSchedulerForCacheRefresh();
} else {
// this means that an invalidation is already scheduled; as long as we're synchronized on scheduledInvalidationInterval
// we can safely extend the interval; the invalidation won't start before we release the lock
scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation);
}
alreadyContained = scheduledRefreshInterval.contains(startOfInvalidation.getObject().getTimePoint(), endOfInvalidation);
} finally {
LockUtil.unlockAfterWrite(cacheLock);
LockUtil.unlockAfterRead(scheduledRefreshIntervalLock);
}
if (!alreadyContained) {
LockUtil.lockForWrite(scheduledRefreshIntervalLock);
try {
if (!scheduledRefreshInterval.isSet()) {
// according to the invariant this implies [1]==null
scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation);
startSchedulerForCacheRefresh();
} else {
// this means that an invalidation or incremental refresh is already scheduled; as long as we're
// synchronized on scheduledInvalidationInterval we can safely extend the interval; the invalidation
// won't start before we release the lock
scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation);
}
} finally {
LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock);
}
}
}
@@ -285,6 +371,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
* running.
*/
private void invalidateCache() {
LockUtil.lockForWrite(scheduledRefreshIntervalLock);
LockUtil.lockForWrite(cacheLock);
try {
Iterator<WindWithConfidence<TimePoint>> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes()
@@ -312,6 +399,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
scheduledRefreshInterval.clear();
} finally {
LockUtil.unlockAfterWrite(cacheLock);
LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock);
}
}
@@ -321,27 +409,33 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
private void refreshCacheIncrementally() {
Set<WindWithConfidence<TimePoint>> windFixesToRecalculate = new HashSet<WindWithConfidence<TimePoint>>();
Set<TimePoint> cachedNullResultsToRecalculate = new HashSet<TimePoint>();
final WindWithConfidence<TimePoint> refreshIntervalStart;
final TimePoint refreshIntervalEnd;
LockUtil.lockForRead(scheduledRefreshIntervalLock);
LockUtil.lockForRead(cacheLock);
try {
Iterator<WindWithConfidence<TimePoint>> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes()
: getCachedFixes().tailSet(scheduledRefreshInterval.getStart(), /* inclusive */true)).iterator();
Iterator<TimePoint> nullIter = (scheduledRefreshInterval.getStart() == null ? timePointsWithCachedNullResult
: timePointsWithCachedNullResult.tailSet(scheduledRefreshInterval.getStart().getObject().getTimePoint(), /* inclusive */
refreshIntervalStart = scheduledRefreshInterval.getStart();
Iterator<WindWithConfidence<TimePoint>> iter = (refreshIntervalStart == null ? getCachedFixes()
: getCachedFixes().tailSet(refreshIntervalStart, /* inclusive */true)).iterator();
Iterator<TimePoint> nullIter = (refreshIntervalStart == null ? timePointsWithCachedNullResult
: timePointsWithCachedNullResult.tailSet(refreshIntervalStart.getObject().getTimePoint(), /* inclusive */
true)).iterator();
WindWithConfidence<TimePoint> nextFixToRecalculate = null;
refreshIntervalEnd = scheduledRefreshInterval.getEnd();
while (iter.hasNext() &&
((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(scheduledRefreshInterval.getEnd()) < 0) ||
scheduledRefreshInterval.getEnd() == null) {
((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(refreshIntervalEnd) < 0) ||
refreshIntervalEnd == null) {
windFixesToRecalculate.add(nextFixToRecalculate);
}
TimePoint nextNullResultToRecalculate = null;
while (nullIter.hasNext() &&
((nextNullResultToRecalculate = nullIter.next()).compareTo(scheduledRefreshInterval.getEnd()) < 0) ||
scheduledRefreshInterval.getEnd() == null) {
((nextNullResultToRecalculate = nullIter.next()).compareTo(refreshIntervalEnd) < 0) ||
refreshIntervalEnd == null) {
cachedNullResultsToRecalculate.add(nextNullResultToRecalculate);
}
} finally {
LockUtil.unlockAfterRead(cacheLock);
LockUtil.unlockAfterRead(scheduledRefreshIntervalLock);
}
Set<TimePoint> nullRemovals = new HashSet<TimePoint>();
Set<TimePoint> nullInsertions = new HashSet<TimePoint>();
@@ -381,10 +475,22 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
for (Map.Entry<TimePoint, WindWithConfidence<TimePoint>> cacheInsertion : cacheInsertions.entrySet()) {
cache(cacheInsertion.getKey(), cacheInsertion.getValue());
}
scheduledRefreshInterval.clear();
} finally {
LockUtil.unlockAfterWrite(cacheLock);
}
LockUtil.lockForWrite(scheduledRefreshIntervalLock);
try {
// now remove the interval for which the cache was refreshed; note that in between the interval may have been extended;
// for the extended part of the interval that hasn't been refreshed during this run, a new refresh needs to be
// requested
InvalidationInterval remainingRefreshInterval = scheduledRefreshInterval.subtract(refreshIntervalStart, refreshIntervalEnd);
scheduledRefreshInterval.clear();
if (remainingRefreshInterval.isSet()) {
scheduleCacheRefresh(remainingRefreshInterval.getStart(), remainingRefreshInterval.getEnd());
}
} finally {
LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock);
}
}
private void startSchedulerForCacheRefresh() {
@@ -452,15 +558,18 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl
return result;
}
private WindWithConfidence<TimePoint> getDummyFixWithConfidence(TimePoint timePoint) {
private static WindWithConfidence<TimePoint> getDummyFixWithConfidence(TimePoint timePoint) {
return new WindWithConfidenceImpl<TimePoint>(new WindImpl(null, timePoint, defaultSpeedWithBearing), 0,
timePoint, /* useSpeed */false);
}
private boolean nullResultCacheContains(TimePoint timePoint) {
assertReadLock();
synchronized (timePointsWithCachedNullResultFastContains) {
LockUtil.lockForRead(cacheLock);
try {
return timePointsWithCachedNullResultFastContains.contains(timePoint);
} finally {
LockUtil.unlockAfterRead(cacheLock);
}
}
@@ -52,10 +52,6 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl
*/
abstract protected Wind getWind(Position p, TimePoint timePoint);
public long getResolutionInMilliseconds() {
return resolutionInMilliseconds;
}
/**
* @param from expected to be an integer multiple of {@link #resolutionInMilliseconds} or <code>null</code>
* @param to expected to be an integer multiple of {@link #resolutionInMilliseconds} or <code>null</code>
@@ -71,6 +67,10 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl
this.resolutionInMilliseconds = resolutionInMilliseconds;
}
public long getResolutionInMilliseconds() {
return resolutionInMilliseconds;
}
protected WindTrack getTrack() {
return track;
}
@@ -132,10 +132,14 @@ public class CompetitorEditDialog extends DataEntryDialog<CompetitorDTO> {
@Override
protected CompetitorDTO getResult() {
Color color;
try {
color = new RGBColor(displayColorTextBox.getText());
} catch (IllegalArgumentException iae) {
color = new InvalidColor(iae);
if (displayColorTextBox.getText() == null || displayColorTextBox.getText().isEmpty()) {
color = null;
} else {
try {
color = new RGBColor(displayColorTextBox.getText());
} catch (IllegalArgumentException iae) {
color = new InvalidColor(iae);
}
}
CompetitorDTO result = new CompetitorDTOImpl(name.getText(), color,
/* twoLetterIsoCountryCode */ null,
@@ -373,6 +373,7 @@ public interface StringMessages extends Messages {
String scoringSchemeLowPointSystem();
String scoringSchemeHighPointSystem();
String scoringSchemeLowPointWinnerGetsZero();
String scoringSchemeWinnerGetsSix();
String scoringSystem();
String createFlexibleLeaderboard();
String createRegattaLeaderboard();
@@ -402,6 +402,7 @@ scoringSchemeHighPointSystem=High Point System
scoringSchemeHighPointEssOverall=High Point System ESS Overall
scoringSchemeHighPointLastBreaksTie=High Point System, Last Race Breaks Tie
scoringSchemeLowPointWinnerGetsZero=Low Point System, Winner Gets Zero
scoringSchemeWinnerGetsSix=High Point System, Winner Gets 6 Points
scoringSystem=Scoring system
createFlexibleLeaderboard=Create flexible leaderboard
createRegattaLeaderboard=Create regatta leaderboard
@@ -403,6 +403,7 @@ scoringSchemeHighPointSystem=High Point System
scoringSchemeHighPointEssOverall=High Point System ESS Gesamtwertung
scoringSchemeHighPointLastBreaksTie=High Point System, letztes Rennen entscheidet bei Gleichstand
scoringSchemeLowPointWinnerGetsZero=Low Point System, Gewinner erhält 0 Punkte
scoringSchemeWinnerGetsSix=High Point System, Gewinner erhält 6 Punkte
scoringSystem=Bepunktungssystem
createFlexibleLeaderboard=Flexible Rangliste anlegen
createRegattaLeaderboard=Rangliste für Regatta anlegen
@@ -20,6 +20,8 @@ public class ScoringSchemeTypeFormatter {
return stringMessages.scoringSchemeHighPointFirstGetsTen();
case LOW_POINT_WINNER_GETS_ZERO:
return stringMessages.scoringSchemeLowPointWinnerGetsZero();
case HIGH_POINT_WINNER_GETS_SIX:
return stringMessages.scoringSchemeWinnerGetsSix();
}
return null;
}
@@ -6,7 +6,6 @@ import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import com.sap.sailing.domain.base.Person;
import com.sap.sailing.domain.base.SharedDomainFactory;
import com.sap.sailing.domain.base.impl.DynamicPerson;
import com.sap.sailing.domain.base.impl.DynamicTeam;
@@ -32,7 +31,7 @@ public class TeamJsonDeserializer implements JsonDeserializer<DynamicTeam> {
public DynamicTeam deserialize(JSONObject object)
throws JsonDeserializationException {
String name = (String) object.get(TeamJsonSerializer.FIELD_NAME);
Person coach = personDeserializer.deserialize((JSONObject) object
DynamicPerson coach = personDeserializer.deserialize((JSONObject) object
.get(TeamJsonSerializer.FIELD_COACH));
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
@@ -37,7 +37,6 @@ import com.sap.sailing.domain.base.CourseArea;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Event;
import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.Person;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Series;
@@ -193,7 +192,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -204,7 +203,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
"segelt auch mit"));
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
"Bester Coach");
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
@@ -402,7 +401,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -412,7 +411,7 @@ public class MasterDataImportTest {
UUID competitor2UUID = UUID.randomUUID();
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Test Mustermann", new NationalityImpl("GER"), new Date(645487200000L), "desc"));
Person coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
DynamicPerson coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
DynamicTeam team2 = new TeamImpl("Pros2", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("FastBoat", boatClass, "GER70133");
CompetitorImpl competitor2 = new CompetitorImpl(competitor2UUID, "Froderik", Color.RED, team2, boat2);
@@ -678,7 +677,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -689,7 +688,7 @@ public class MasterDataImportTest {
UUID competitor2UUID = UUID.randomUUID();
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Test Mustermann", new NationalityImpl("GER"), new Date(645487200000L), "desc"));
Person coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
DynamicPerson coach2 = new PersonImpl("Max Test", new NationalityImpl("GER"), new Date(645487200000L), "desc");
DynamicTeam team2 = new TeamImpl("Pros2", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("FastBoat", boatClass, "GER70133");
Competitor competitor2 = sourceDomainFactory.getCompetitorStore().getOrCreateCompetitor(competitor2UUID,
@@ -833,7 +832,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -844,7 +843,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
"segelt auch mit"));
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
"Bester Coach");
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
@@ -1029,7 +1028,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -1040,7 +1039,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
"segelt auch mit"));
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
"Bester Coach");
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
@@ -1127,7 +1126,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailorsToOverride = new HashSet<DynamicPerson>();
sailorsToOverride.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(
645487200000L), "Oberhoschy"));
Person coachToOverride = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(
DynamicPerson coachToOverride = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(
645487200000L), "Der Lennart halt");
DynamicTeam teamToOverride = new TeamImpl("Pros", sailorsToOverride, coachToOverride);
BoatClass boatClassToOverride = new BoatClassImpl("H16", true);
@@ -1298,7 +1297,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -1309,7 +1308,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
"segelt auch mit"));
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
"Bester Coach");
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
@@ -1431,7 +1430,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -1442,7 +1441,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
"segelt auch mit"));
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
"Bester Coach");
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
@@ -1638,7 +1637,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors = new HashSet<DynamicPerson>();
sailors.add(new PersonImpl("Froderik Poterson", new NationalityImpl("GER"), new Date(645487200000L),
"Oberhoschy"));
Person coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach = new PersonImpl("Lennart Hensler", new NationalityImpl("GER"), new Date(645487200000L),
"Der Lennart halt");
DynamicTeam team = new TeamImpl("Pros", sailors, coach);
BoatClass boatClass = new BoatClassImpl("H16", true);
@@ -1649,7 +1648,7 @@ public class MasterDataImportTest {
Set<DynamicPerson> sailors2 = new HashSet<DynamicPerson>();
sailors2.add(new PersonImpl("Angela Merkel", new NationalityImpl("GER"), new Date(645487200000L),
"segelt auch mit"));
Person coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
DynamicPerson coach2 = new PersonImpl("Peer Steinbrueck", new NationalityImpl("GER"), new Date(645487200000L),
"Bester Coach");
DynamicTeam team2 = new TeamImpl("Noobs", sailors2, coach2);
DynamicBoat boat2 = new BoatImpl("LahmeEnte", boatClass, "GER1337");
@@ -29,7 +29,9 @@ public class UpdateCompetitor extends AbstractRacingEventServiceOperation<Compet
@Override
public Competitor internalApplyTo(RacingEventService toState) throws Exception {
return toState.getBaseDomainFactory().getCompetitorStore().updateCompetitor(idAsString, newName, newDisplayColor, newSailId, newNationality);
Competitor result = toState.getBaseDomainFactory().getCompetitorStore()
.updateCompetitor(idAsString, newName, newDisplayColor, newSailId, newNationality);
return result;
}
@Override
+25 -2
View File
@@ -95,7 +95,30 @@
<a href='http://www.extremesailingseries.com' target='_blank'>Official website</a>
</div>
</div>
<div class='raceSection'>
<div class='raceSection'>
<h5 class='date'>
2013 Season
</h5>
<div class='raceTimepoint'></div>
<a href='http://bundesliga2013.sapsailing.com' target='_blank'>
<ul class='racePanel'>
<li class='raceName'>
German Sailing League / Bundesliga 2013
</li>
<li class='raceLocation'>
Germany
</li>
</ul>
</a>
<div class='raceDescription'>
<p>German Sailing League / Bundesliga 2013</p>
<a href='http://bundesliga2013.sapsailing.com' target='_blank'>SAP Sailing Analytics</a><br/>
<a href='http://segelbundesliga.de' target='_blank'>Official website</a>
</div>
</div>
<div class='raceSection'>
<h5 class='date'>
Sep 19-22
</h5>
@@ -115,7 +138,7 @@
<a href='http://505idm2013.sapsailing.com' target='_blank'>SAP Sailing Analytics</a><br/>
<a href='http://505idm2013.de' target='_blank'>Official website</a>
</div>
</div>
</div>
<div class='raceSection'>
<h5 class='date'>
July 19 - 28
@@ -38,6 +38,12 @@
<h2 class="articleSubheadline">May 2014</h2>
<ul class="bulletList">
<li>A hint regarding the "distance to start line" metrics: please keep in mind that currently this
measurement is calculated as the distance of the GPS tracker from the line. Unless the tracker
is mounted near the bow (or at the bow sprit, if any), the distance calculated this way will
be greater than what counts for crossing the line. Ordering, comparing and aggregating over this
figure still makes sense as long as you remember this detail.
</li>
<li>Introduced a new leaderboard entry "XTE +/-" featuring a signed cross-track error which shows
for a leg and also aggregated for an entire race on which side of the leg a competitor stayed on
average. Currently, the "side" is defined as viewing in the leg's direction, so "left" for a downwind