mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-10 14:20:55 +00:00
Merge branch 'master' into dataMining
This commit is contained in:
commit
86c53acd77
20 files changed
+671
-101
No files matched your search
@@ -480,7 +480,9 @@ if [[ "$@" == "build" ]] || [[ "$@" == "all" ]]; then
|
||||
|
||||
echo "Using following command: mvn $extra -DargLine=\"$APP_PARAMETERS\" -fae -s $MAVEN_SETTINGS $clean install"
|
||||
echo "Maven version used: `mvn --version`"
|
||||
(mvn $extra -DargLine="$APP_PARAMETERS" -fae -s $MAVEN_SETTINGS $clean install; export MVN_EXIT_CODE=$?) 2>&1 | tee $START_DIR/build.log
|
||||
mvn $extra -DargLine="$APP_PARAMETERS" -fae -s $MAVEN_SETTINGS $clean install 2>&1 | tee $START_DIR/build.log
|
||||
# now get the exit status from mvn, and not that of tee which is what $? contains now
|
||||
MVN_EXIT_CODE=${PIPESTATUS[0]}
|
||||
echo "Maven exit code is $MVN_EXIT_CODE"
|
||||
|
||||
if [ $reporting -eq 1 ]; then
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ import com.sap.sailing.domain.igtimiadapter.datatypes.COG;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsLatLong;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.HDG;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.SOG;
|
||||
import com.sap.sailing.domain.igtimiadapter.shared.IgtimiWindReceiver;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
|
||||
public class WindReceiverTest {
|
||||
|
||||
+13
-2
@@ -2,6 +2,8 @@ package com.sap.sailing.domain.igtimiadapter.websocket;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -158,7 +160,11 @@ public class WebSocketTest {
|
||||
// the data from baur@stg-academy.org, particularly containing the Berlin test data
|
||||
Account account = igtimiConnectionFactory.registerAccountForWhichClientIsAuthorized("9fded995cf21c8ed91ddaec13b220e8d5e44c65808d22ec2b1b7c32261121f26");
|
||||
IgtimiConnection conn = igtimiConnectionFactory.connect(account);
|
||||
LiveDataConnection liveDataConnection = conn.createLiveConnection(Collections.singleton("GA-EN-AAEJ"));
|
||||
LiveDataConnection liveDataConnection = conn.getOrCreateLiveConnection(Collections.singleton("GA-EN-AAEJ"));
|
||||
LiveDataConnection redundantSecondSharedConnection = conn.getOrCreateLiveConnection(Collections.singleton("GA-EN-AAEJ"));
|
||||
assertTrue(liveDataConnection instanceof LiveDataConnectionWrapper);
|
||||
assertTrue(redundantSecondSharedConnection instanceof LiveDataConnectionWrapper);
|
||||
assertSame(((LiveDataConnectionWrapper) liveDataConnection).getActualConnection(), ((LiveDataConnectionWrapper) redundantSecondSharedConnection).getActualConnection());
|
||||
liveDataConnection.addListener(new BulkFixReceiver() {
|
||||
@Override
|
||||
public void received(Iterable<Fix> fixes) {
|
||||
@@ -167,6 +173,11 @@ public class WebSocketTest {
|
||||
});
|
||||
assertNotNull(liveDataConnection);
|
||||
assertTrue("Connection handshake not successful within 5s", liveDataConnection.waitForConnection(5000l));
|
||||
liveDataConnection.stop();
|
||||
liveDataConnection.stop(); // this won't stop the actual connection because it's still shared with redundantSecondSharedConnection
|
||||
redundantSecondSharedConnection.stop(); // now this should stop the actual connection
|
||||
LiveDataConnection secondRedundantSecondSharedConnection = conn.getOrCreateLiveConnection(Collections.singleton("GA-EN-AAEJ"));
|
||||
assertTrue(secondRedundantSecondSharedConnection instanceof LiveDataConnectionWrapper);
|
||||
// a new actual connection is expected to have been created
|
||||
assertNotSame(((LiveDataConnectionWrapper) liveDataConnection).getActualConnection(), ((LiveDataConnectionWrapper) secondRedundantSecondSharedConnection).getActualConnection());
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,5 @@ Import-Package: javax.ws.rs;version="1.1.1",
|
||||
Web-ContextPath: /igtimi
|
||||
Bundle-Activator: com.sap.sailing.domain.igtimiadapter.impl.Activator
|
||||
Export-Package: com.sap.sailing.domain.igtimiadapter,
|
||||
com.sap.sailing.domain.igtimiadapter.datatypes
|
||||
com.sap.sailing.domain.igtimiadapter.datatypes,
|
||||
com.sap.sailing.domain.igtimiadapter.shared
|
||||
+7
-1
@@ -96,7 +96,7 @@ public interface IgtimiConnection {
|
||||
*
|
||||
* @return a connection that the caller can use to stop the live feed by calling {@link LiveDataConnection#stop()}.
|
||||
*/
|
||||
LiveDataConnection createLiveConnection(Iterable<String> deviceSerialNumbers) throws Exception;
|
||||
LiveDataConnection getOrCreateLiveConnection(Iterable<String> deviceSerialNumbers) throws Exception;
|
||||
|
||||
/**
|
||||
* @param sessionIds
|
||||
@@ -154,6 +154,12 @@ public interface IgtimiConnection {
|
||||
*/
|
||||
Map<TrackedRace, Integer> importWindIntoRace(Iterable<DynamicTrackedRace> trackedRaces) throws IllegalStateException,
|
||||
ClientProtocolException, IOException, ParseException;
|
||||
|
||||
/**
|
||||
* Find all the devices from which we may read and which have logged GPS positions and apparent wind speed (AWS) or that
|
||||
* have never logged GPS nor wind (probably new sensors)
|
||||
*/
|
||||
Iterable<String> getWindDevices() throws IllegalStateException, IOException, ParseException;
|
||||
|
||||
/**
|
||||
* Returns the latest datum for the specified devices that contains a fix of the <code>type</code> requested. The
|
||||
|
||||
+46
-3
@@ -34,7 +34,9 @@ import com.sap.sailing.domain.igtimiadapter.Session;
|
||||
import com.sap.sailing.domain.igtimiadapter.User;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.Type;
|
||||
import com.sap.sailing.domain.igtimiadapter.websocket.WebSocketConnectionManager;
|
||||
import com.sap.sailing.domain.igtimiadapter.shared.IgtimiWindReceiver;
|
||||
import com.sap.sailing.domain.igtimiadapter.websocket.LiveDataConnectionFactory;
|
||||
import com.sap.sailing.domain.igtimiadapter.websocket.LiveDataConnectionFactoryImpl;
|
||||
import com.sap.sailing.domain.tracking.DynamicTrack;
|
||||
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
@@ -44,10 +46,12 @@ public class IgtimiConnectionImpl implements IgtimiConnection {
|
||||
private static final Logger logger = Logger.getLogger(IgtimiConnectionImpl.class.getName());
|
||||
private final Account account;
|
||||
private final IgtimiConnectionFactoryImpl connectionFactory;
|
||||
private final LiveDataConnectionFactory liveDataConnectionFactory;
|
||||
|
||||
public IgtimiConnectionImpl(IgtimiConnectionFactoryImpl connectionFactory, Account account) {
|
||||
this.connectionFactory = connectionFactory;
|
||||
this.account = account;
|
||||
liveDataConnectionFactory = new LiveDataConnectionFactoryImpl(connectionFactory, account);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -183,8 +187,8 @@ public class IgtimiConnectionImpl implements IgtimiConnection {
|
||||
|
||||
|
||||
@Override
|
||||
public LiveDataConnection createLiveConnection(Iterable<String> deviceSerialNumbers) throws Exception {
|
||||
return new WebSocketConnectionManager(connectionFactory, deviceSerialNumbers, getAccount());
|
||||
public LiveDataConnection getOrCreateLiveConnection(Iterable<String> deviceSerialNumbers) throws Exception {
|
||||
return liveDataConnectionFactory.getOrCreateLiveDataConnection(deviceSerialNumbers);
|
||||
}
|
||||
|
||||
private DynamicTrack<Fix> getOrCreateTrack(Map<String, Map<Type, DynamicTrack<Fix>>> result,
|
||||
@@ -287,4 +291,43 @@ public class IgtimiConnectionImpl implements IgtimiConnection {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> getWindDevices() throws IllegalStateException, ClientProtocolException, IOException, ParseException {
|
||||
// find all the devices from which we may read
|
||||
Iterable<DataAccessWindow> dataAccessWindows = getDataAccessWindows(
|
||||
Permission.read, /* start time */ null, /* end time */ null,
|
||||
/* get data for all available deviceSerialNumbers */null);
|
||||
Set<String> deviceSerialNumbersWeCanRead = new HashSet<>();
|
||||
for (DataAccessWindow daw : dataAccessWindows) {
|
||||
deviceSerialNumbersWeCanRead.add(daw.getDeviceSerialNumber());
|
||||
}
|
||||
// find all that haven't even sent GPS; those may never have sent ever, so we need to listen to them for new stuff; they could be wind sensors
|
||||
Iterable<Fix> gpsFixes = getLatestFixes(deviceSerialNumbersWeCanRead, Type.gps_latlong);
|
||||
Set<String> devicesWithGps = getDeviceSerialNumbers(gpsFixes);
|
||||
Iterable<Fix> awsFixes = getLatestFixes(deviceSerialNumbersWeCanRead, Type.AWS); // look for latest fixes with apparent wind speed in the fix
|
||||
Set<String> devicesWithWind = getDeviceSerialNumbers(awsFixes);
|
||||
Set<String> devicesThatHaveNeverSentGpsNorWind = new HashSet<>(deviceSerialNumbersWeCanRead);
|
||||
devicesThatHaveNeverSentGpsNorWind.removeAll(devicesWithGps);
|
||||
devicesThatHaveNeverSentGpsNorWind.removeAll(devicesWithWind);
|
||||
Set<String> devicesWeShouldListenTo = new HashSet<>();
|
||||
devicesWeShouldListenTo.addAll(devicesWithWind);
|
||||
devicesWeShouldListenTo.addAll(devicesThatHaveNeverSentGpsNorWind);
|
||||
logger.info("Wind devices identified: "
|
||||
+ devicesWeShouldListenTo
|
||||
+ " because from all devices "+deviceSerialNumbersWeCanRead+" for "
|
||||
+ devicesThatHaveNeverSentGpsNorWind
|
||||
+ " we don't know what they are as they never sent anything we can access, and for "
|
||||
+ devicesWithWind + " we know they sent wind");
|
||||
return devicesWeShouldListenTo;
|
||||
}
|
||||
|
||||
private Set<String> getDeviceSerialNumbers(Iterable<Fix> fixes) {
|
||||
Set<String> deviceSerialNumbers = new HashSet<>();
|
||||
for (Fix fix : fixes) {
|
||||
deviceSerialNumbers.add(fix.getSensor().getDeviceSerialNumber());
|
||||
}
|
||||
return deviceSerialNumbers;
|
||||
}
|
||||
|
||||
}
|
||||
+6
-43
@@ -4,10 +4,8 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -15,13 +13,10 @@ import com.sap.sailing.domain.common.TimePoint;
|
||||
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
|
||||
import com.sap.sailing.domain.common.impl.Util.Pair;
|
||||
import com.sap.sailing.domain.igtimiadapter.Account;
|
||||
import com.sap.sailing.domain.igtimiadapter.DataAccessWindow;
|
||||
import com.sap.sailing.domain.igtimiadapter.IgtimiConnection;
|
||||
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
|
||||
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
|
||||
import com.sap.sailing.domain.igtimiadapter.Permission;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.Type;
|
||||
import com.sap.sailing.domain.igtimiadapter.shared.IgtimiWindReceiver;
|
||||
import com.sap.sailing.domain.tracking.AbstractWindTracker;
|
||||
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
|
||||
import com.sap.sailing.domain.tracking.WindTracker;
|
||||
@@ -30,7 +25,7 @@ public class IgtimiWindTracker extends AbstractWindTracker implements WindTracke
|
||||
private static final Logger logger = Logger.getLogger(IgtimiWindTracker.class.getName());
|
||||
private static final int TIME_INTERVAL_TO_TRACK_BEFORE_RACE_START_MILLIS = 10*60*1000; // 10 minutes
|
||||
private static final long TIME_INTERVAL_TO_TRACK_AFTER_END_OF_RACE_MILLIS = 60*60*1000; // 60 minutes
|
||||
private final Map<LiveDataConnection, Pair<Set<String>, Account>> liveConnectionsAndDeviceSerialNumber;
|
||||
private final Map<LiveDataConnection, Pair<Iterable<String>, Account>> liveConnectionsAndDeviceSerialNumber;
|
||||
private final IgtimiWindTrackerFactory windTrackerFactory;
|
||||
private boolean stopping;
|
||||
|
||||
@@ -49,37 +44,13 @@ public class IgtimiWindTracker extends AbstractWindTracker implements WindTracke
|
||||
try {
|
||||
if (!stopping) {
|
||||
IgtimiConnection connection = connectionFactory.connect(account);
|
||||
// find all the devices from which we may read
|
||||
Iterable<DataAccessWindow> dataAccessWindows = connection.getDataAccessWindows(
|
||||
Permission.read, /* start time */ null, /* end time */ null,
|
||||
/* get data for all available deviceSerialNumbers */null);
|
||||
Set<String> deviceSerialNumbersWeCanRead = new HashSet<>();
|
||||
for (DataAccessWindow daw : dataAccessWindows) {
|
||||
deviceSerialNumbersWeCanRead.add(daw.getDeviceSerialNumber());
|
||||
}
|
||||
// find all that haven't even sent GPS; those may never have sent ever, so we need to listen to them for new stuff; they could be wind sensors
|
||||
Iterable<Fix> gpsFixes = connection.getLatestFixes(deviceSerialNumbersWeCanRead, Type.gps_latlong);
|
||||
Set<String> devicesWithGps = getDeviceSerialNumbers(gpsFixes);
|
||||
Iterable<Fix> awsFixes = connection.getLatestFixes(deviceSerialNumbersWeCanRead, Type.AWS); // look for latest fixes with apparent wind speed in the fix
|
||||
Set<String> devicesWithWind = getDeviceSerialNumbers(awsFixes);
|
||||
Set<String> devicesThatHaveNeverSentGpsNorWind = new HashSet<>(deviceSerialNumbersWeCanRead);
|
||||
devicesThatHaveNeverSentGpsNorWind.removeAll(devicesWithGps);
|
||||
devicesThatHaveNeverSentGpsNorWind.removeAll(devicesWithWind);
|
||||
Set<String> devicesWeShouldListenTo = new HashSet<>();
|
||||
devicesWeShouldListenTo.addAll(devicesWithWind);
|
||||
devicesWeShouldListenTo.addAll(devicesThatHaveNeverSentGpsNorWind);
|
||||
logger.info("Will listen to devices "
|
||||
+ devicesWeShouldListenTo
|
||||
+ " because from all devices "+deviceSerialNumbersWeCanRead+" for "
|
||||
+ devicesThatHaveNeverSentGpsNorWind
|
||||
+ " we don't know what they are as they never sent anything we can access, and for "
|
||||
+ devicesWithWind + " we know they sent wind");
|
||||
Iterable<String> devicesWeShouldListenTo = connection.getWindDevices();
|
||||
if (!stopping) {
|
||||
LiveDataConnection liveConnection = connection.createLiveConnection(devicesWeShouldListenTo);
|
||||
LiveDataConnection liveConnection = connection.getOrCreateLiveConnection(devicesWeShouldListenTo);
|
||||
IgtimiWindReceiver windReceiver = new IgtimiWindReceiver(devicesWeShouldListenTo);
|
||||
liveConnection.addListener(windReceiver);
|
||||
windReceiver.addListener(new WindListenerSendingToTrackedRace(Collections.singleton(getTrackedRace()), windTrackerFactory));
|
||||
liveConnectionsAndDeviceSerialNumber.put(liveConnection, new Pair<Set<String>, Account>(devicesWeShouldListenTo, account));
|
||||
liveConnectionsAndDeviceSerialNumber.put(liveConnection, new Pair<Iterable<String>, Account>(devicesWeShouldListenTo, account));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -92,14 +63,6 @@ public class IgtimiWindTracker extends AbstractWindTracker implements WindTracke
|
||||
}.start();
|
||||
}
|
||||
|
||||
private Set<String> getDeviceSerialNumbers(Iterable<Fix> fixes) {
|
||||
Set<String> deviceSerialNumbers = new HashSet<>();
|
||||
for (Fix fix : fixes) {
|
||||
deviceSerialNumbers.add(fix.getSensor().getDeviceSerialNumber());
|
||||
}
|
||||
return deviceSerialNumbers;
|
||||
}
|
||||
|
||||
public static TimePoint getReceivingEndTime(DynamicTrackedRace trackedRace) {
|
||||
TimePoint endOfRace = trackedRace.getEndOfRace();
|
||||
TimePoint endOfTracking = trackedRace.getEndOfTracking();
|
||||
@@ -151,7 +114,7 @@ public class IgtimiWindTracker extends AbstractWindTracker implements WindTracke
|
||||
logger.info("Stopping Igtimi live connection "+ldc);
|
||||
ldc.stop();
|
||||
} catch (Exception e) {
|
||||
final Pair<Set<String>, Account> deviceSerialNumberAndAccount = liveConnectionsAndDeviceSerialNumber.get(ldc);
|
||||
final Pair<Iterable<String>, Account> deviceSerialNumberAndAccount = liveConnectionsAndDeviceSerialNumber.get(ldc);
|
||||
logger.log(Level.INFO,
|
||||
"Exception trying to stop Igtimi live connection for wind receiver for race "
|
||||
+ getTrackedRace().getRace() + " and device " + deviceSerialNumberAndAccount.getA()
|
||||
|
||||
+14
-4
@@ -1,4 +1,4 @@
|
||||
package com.sap.sailing.domain.igtimiadapter.impl;
|
||||
package com.sap.sailing.domain.igtimiadapter.shared;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
@@ -138,12 +138,18 @@ public class IgtimiWindReceiver implements BulkFixReceiver {
|
||||
}
|
||||
});
|
||||
}
|
||||
logger.info("Received "+Util.size(awsFixes)+" wind fixes");
|
||||
logger.fine("Received "+Util.size(awsFixes)+" wind fixes");
|
||||
boolean loggedWindFixGenerationProblem = false;
|
||||
for (AWS aws : awsFixes) {
|
||||
try {
|
||||
final Wind wind = getWind(aws.getTimePoint());
|
||||
if (wind != null) {
|
||||
notifyListeners(wind, aws.getSensor().getDeviceSerialNumber());
|
||||
} else {
|
||||
if (!loggedWindFixGenerationProblem) {
|
||||
logger.info("Not enough information to build a Wind fix out of data provided. AWS received but most probably HDG or HDGM not received (yet) - check your compass.");
|
||||
loggedWindFixGenerationProblem = true;
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException | IOException | ParseException e) {
|
||||
logger.log(Level.INFO, "Exception while trying to construct Wind fix from Igtimi fix " + aws, e);
|
||||
@@ -358,8 +364,12 @@ public class IgtimiWindReceiver implements BulkFixReceiver {
|
||||
trueHeading = hdg;
|
||||
} else {
|
||||
Bearing hdgm = getHDGM(timePoint, hdgmPair);
|
||||
Declination declination = declinationService.getDeclination(timePoint, position, /* timeoutForOnlineFetchInMilliseconds 5s */ 5000);
|
||||
trueHeading = hdgm.add(declination.getBearingCorrectedTo(timePoint));
|
||||
if (hdgm != null) {
|
||||
Declination declination = declinationService.getDeclination(timePoint, position, /* timeoutForOnlineFetchInMilliseconds 5s */ 5000);
|
||||
trueHeading = hdgm.add(declination.getBearingCorrectedTo(timePoint));
|
||||
} else {
|
||||
trueHeading = null;
|
||||
}
|
||||
}
|
||||
return trueHeading;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.sap.sailing.domain.igtimiadapter.websocket;
|
||||
|
||||
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
|
||||
|
||||
/**
|
||||
* Helps bundling live data connections for the same set of devices. Clients can request a connection for a set of devices.
|
||||
* If one already exists, a wrapper to it is returned. This wrapper's {@link LiveDataConnection#stop()} method work such that
|
||||
* it only decrements a usage counter in this factory (and does so at most once), such that the actual connection is only
|
||||
* terminated if the last client has stopped using it.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
public interface LiveDataConnectionFactory {
|
||||
LiveDataConnection getOrCreateLiveDataConnection(Iterable<String> deviceSerialNumbers) throws Exception;
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.sap.sailing.domain.igtimiadapter.websocket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.sap.sailing.domain.common.impl.Util;
|
||||
import com.sap.sailing.domain.igtimiadapter.Account;
|
||||
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
|
||||
import com.sap.sailing.domain.igtimiadapter.impl.IgtimiConnectionFactoryImpl;
|
||||
|
||||
public class LiveDataConnectionFactoryImpl implements LiveDataConnectionFactory {
|
||||
private static final Logger logger = Logger.getLogger(LiveDataConnectionFactoryImpl.class.getName());
|
||||
private final IgtimiConnectionFactoryImpl connectionFactory;
|
||||
private final Account account;
|
||||
private final Map<Set<String>, LiveDataConnection> dataConnectionsForDeviceSerialNumbers;
|
||||
private final Map<LiveDataConnection, Set<String>> deviceSerialNumersForDataConnections;
|
||||
private final Map<LiveDataConnection, Integer> usageCounts;
|
||||
|
||||
public LiveDataConnectionFactoryImpl(IgtimiConnectionFactoryImpl connectionFactory, Account account) {
|
||||
this.connectionFactory = connectionFactory;
|
||||
this.account = account;
|
||||
dataConnectionsForDeviceSerialNumbers = new HashMap<>();
|
||||
deviceSerialNumersForDataConnections = new HashMap<>();
|
||||
usageCounts = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized LiveDataConnection getOrCreateLiveDataConnection(Iterable<String> deviceSerialNumbers) throws Exception {
|
||||
Set<String> deviceSerialNumbersAsSet = new HashSet<>();
|
||||
Util.addAll(deviceSerialNumbers, deviceSerialNumbersAsSet);
|
||||
LiveDataConnection result = dataConnectionsForDeviceSerialNumbers.get(deviceSerialNumbersAsSet);
|
||||
if (result == null) {
|
||||
result = new WebSocketConnectionManager(connectionFactory, deviceSerialNumbers, account);
|
||||
dataConnectionsForDeviceSerialNumbers.put(deviceSerialNumbersAsSet, result);
|
||||
deviceSerialNumersForDataConnections.put(result, deviceSerialNumbersAsSet);
|
||||
}
|
||||
Integer usageCount = usageCounts.get(result);
|
||||
if (usageCount == null) {
|
||||
usageCount = 0;
|
||||
}
|
||||
usageCount++;
|
||||
usageCounts.put(result, usageCount);
|
||||
return new LiveDataConnectionWrapper(this, result);
|
||||
}
|
||||
|
||||
public synchronized void stop(LiveDataConnection actualConnection) throws Exception {
|
||||
Integer usageCount = usageCounts.get(actualConnection);
|
||||
if (usageCount == null || usageCount == 0) {
|
||||
logger.warning("Strange: the Igtimi live data connection "+actualConnection+" is released by another client although no client should be using it anymore.");
|
||||
} else {
|
||||
usageCount--;
|
||||
if (usageCount == 0) {
|
||||
usageCounts.remove(actualConnection);
|
||||
Set<String> deviceSerialNumbersAsSet = deviceSerialNumersForDataConnections.remove(actualConnection);
|
||||
dataConnectionsForDeviceSerialNumbers.remove(deviceSerialNumbersAsSet);
|
||||
actualConnection.stop();
|
||||
} else {
|
||||
usageCounts.put(actualConnection, usageCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.sap.sailing.domain.igtimiadapter.websocket;
|
||||
|
||||
import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver;
|
||||
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
|
||||
|
||||
public class LiveDataConnectionWrapper implements LiveDataConnection {
|
||||
private final LiveDataConnectionFactoryImpl factory;
|
||||
|
||||
private final LiveDataConnection actualConnection;
|
||||
|
||||
private boolean stopCalled;
|
||||
|
||||
protected LiveDataConnectionWrapper(LiveDataConnectionFactoryImpl factory, LiveDataConnection actualConnection) {
|
||||
super();
|
||||
this.factory = factory;
|
||||
this.actualConnection = actualConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() throws Exception {
|
||||
if (!stopCalled) {
|
||||
factory.stop(actualConnection);
|
||||
stopCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean waitForConnection(long timeoutInMillis) throws InterruptedException {
|
||||
return actualConnection.waitForConnection(timeoutInMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(BulkFixReceiver listener) {
|
||||
actualConnection.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the actual connection available to other classes in this package, particularly the test classes in the test fragment of the same package
|
||||
*/
|
||||
LiveDataConnection getActualConnection() {
|
||||
return actualConnection;
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -47,6 +47,13 @@ public class WebSocketConnectionManager extends WebSocketAdapter implements Live
|
||||
private TimePoint igtimiServerTimepoint;
|
||||
private TimePoint localTimepointWhenServerTimepointWasReceived;
|
||||
|
||||
/**
|
||||
* Counts the messages received. Every {@link #LOG_EVERY_SO_MANY_MESSAGES} an {@link Level#INFO} message is logged.
|
||||
*/
|
||||
private int messageCount;
|
||||
|
||||
private static final int LOG_EVERY_SO_MANY_MESSAGES = 100;
|
||||
|
||||
public WebSocketConnectionManager(IgtimiConnectionFactoryImpl connectionFactory, Iterable<String> deviceSerialNumbers, Account account) throws Exception {
|
||||
this.timer = new Timer("Timer for WebSocketConnectionManager for units "+deviceSerialNumbers+" and account "+account);
|
||||
this.deviceIds = deviceSerialNumbers;
|
||||
@@ -109,6 +116,10 @@ public class WebSocketConnectionManager extends WebSocketAdapter implements Live
|
||||
logger.fine("Received server heartbeat for "+this);
|
||||
receivedServerHeartbeatInInterval = true;
|
||||
} else if (message.startsWith("[")) {
|
||||
messageCount++;
|
||||
if (messageCount % LOG_EVERY_SO_MANY_MESSAGES == 0) {
|
||||
logger.info("Received another "+LOG_EVERY_SO_MANY_MESSAGES+" Igtimi messages. Last message was: "+message);
|
||||
}
|
||||
List<Fix> fixes = new ArrayList<>();
|
||||
try {
|
||||
JSONArray jsonArray = (JSONArray) new JSONParser().parse(message);
|
||||
@@ -219,7 +230,7 @@ public class WebSocketConnectionManager extends WebSocketAdapter implements Live
|
||||
break; // successfully connected
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.log(Level.INFO, "Couldn't connect to "+uri+" for "+this, e);
|
||||
logger.log(Level.SEVERE, "Couldn't connect to "+uri+" for "+this, e);
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.sap.sailing.domain.common.TimePoint;
|
||||
@@ -400,9 +401,7 @@ public class LockUtil {
|
||||
logger.info(message.toString());
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
// re-assert interrupt state that occurred while we
|
||||
// were acquiring the lock
|
||||
Thread.currentThread().interrupt();
|
||||
logger.log(Level.WARNING, "Interrupted while waiting for lock "+lockDescriptionForTimeoutLogMessage, ex);
|
||||
}
|
||||
return locked;
|
||||
}
|
||||
|
||||
@@ -564,8 +564,7 @@ public interface TrackedRace extends Serializable {
|
||||
*
|
||||
* The distance to the line is calculated by projecting the competitor's position onto the line orthogonally and
|
||||
* computing the distance of the projected position and the competitor's position.
|
||||
* <p
|
||||
* .
|
||||
* <p>
|
||||
*
|
||||
* Should the course be empty, <code>null</code> is returned. If the course's first waypoint is not a line or gate,
|
||||
* the geometric distance between the first waypoint and the competitor's position at <code>timePoint</code> is
|
||||
@@ -581,8 +580,7 @@ public interface TrackedRace extends Serializable {
|
||||
*
|
||||
* The distance to the line is calculated by projecting the competitor's position onto the line orthogonally and
|
||||
* computing the distance of the projected position and the competitor's position.
|
||||
* <p
|
||||
* .
|
||||
* <p>
|
||||
*
|
||||
* Should the course be empty, <code>null</code> is returned. If the course's first waypoint is not a line or gate,
|
||||
* the geometric distance between the first waypoint and the competitor's position at <code>timePoint</code> is
|
||||
|
||||
+6
-4
@@ -2370,12 +2370,14 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl
|
||||
|
||||
@Override
|
||||
public Distance getDistanceToStartLine(Competitor competitor, long millisecondsBeforeRaceStart) {
|
||||
final Distance result;
|
||||
if (getStartOfRace() == null) {
|
||||
return null;
|
||||
result = null;
|
||||
} else {
|
||||
TimePoint beforeStart = new MillisecondsTimePoint(getStartOfRace().asMillis() - millisecondsBeforeRaceStart);
|
||||
result = getDistanceToStartLine(competitor, beforeStart);
|
||||
}
|
||||
|
||||
TimePoint beforeStart = new MillisecondsTimePoint(getStartOfRace().asMillis() - millisecondsBeforeRaceStart);
|
||||
return getDistanceToStartLine(competitor, beforeStart);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,7 @@ Import-Package: com.sap.sailing.server,
|
||||
javax.ws.rs;version="1.1.1",
|
||||
javax.ws.rs.core;version="1.1.1",
|
||||
javax.ws.rs.ext;version="1.1.1",
|
||||
org.apache.commons.lang,
|
||||
org.json.simple,
|
||||
org.json.simple.parser,
|
||||
org.osgi.framework;version="1.3.0",
|
||||
@@ -36,7 +37,8 @@ Require-Bundle: com.sap.sailing.domain,
|
||||
javax.validation;bundle-version="1.0.0",
|
||||
com.sun.jersey;bundle-version="1.17.0",
|
||||
org.objectweb.asm;bundle-version="3.3.1",
|
||||
org.apache.commons.fileupload;bundle-version="1.2.2"
|
||||
org.apache.commons.fileupload;bundle-version="1.2.2",
|
||||
com.sap.sailing.domain.igtimiadapter;bundle-version="1.0.0"
|
||||
Bundle-ClassPath: .
|
||||
Web-ContextPath: /sailingserver
|
||||
Export-Package: com.sap.sailing.server.gateway
|
||||
@@ -195,6 +195,14 @@
|
||||
<url-pattern>/expeditionWindStatus</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>WindStatusServlet</servlet-name>
|
||||
<servlet-class>com.sap.sailing.server.gateway.impl.WindStatusServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>WindStatusServlet</servlet-name>
|
||||
<url-pattern>/windStatus</url-pattern>
|
||||
</servlet-mapping>
|
||||
<servlet>
|
||||
<servlet-name>TrackFilesExportPostServlet</servlet-name>
|
||||
<servlet-class>com.sap.sailing.server.gateway.impl.TrackFilesExportPostServlet</servlet-class>
|
||||
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
package com.sap.sailing.server.gateway.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.osgi.util.tracker.ServiceTracker;
|
||||
|
||||
import com.sap.sailing.domain.igtimiadapter.Account;
|
||||
import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver;
|
||||
import com.sap.sailing.domain.igtimiadapter.IgtimiConnection;
|
||||
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
|
||||
import com.sap.sailing.domain.igtimiadapter.IgtimiWindListener;
|
||||
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
|
||||
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
|
||||
import com.sap.sailing.domain.igtimiadapter.shared.IgtimiWindReceiver;
|
||||
import com.sap.sailing.domain.tracking.Wind;
|
||||
import com.sap.sailing.expeditionconnector.ExpeditionListener;
|
||||
import com.sap.sailing.expeditionconnector.ExpeditionMessage;
|
||||
import com.sap.sailing.expeditionconnector.ExpeditionWindTrackerFactory;
|
||||
import com.sap.sailing.expeditionconnector.UDPExpeditionReceiver;
|
||||
import com.sap.sailing.server.gateway.SailingServerHttpServlet;
|
||||
|
||||
/**
|
||||
* Shows the state of wind receivers regardless of them being attached to a race. Currently Expedition and Igtimi are supported.
|
||||
*
|
||||
* @author Simon Marcel Pamies
|
||||
*
|
||||
*/
|
||||
public class WindStatusServlet extends SailingServerHttpServlet implements IgtimiWindListener, BulkFixReceiver {
|
||||
private static final long serialVersionUID = -6791613843435003810L;
|
||||
|
||||
private static List<ExpeditionMessageInfo> lastExpeditionMessages;
|
||||
|
||||
private static int igtimiRawMessageCount;
|
||||
private static List<IgtimiMessageInfo> lastIgtimiMessages;
|
||||
private static IgtimiWindReceiver igtimiWindReceiver;
|
||||
private static LiveDataConnection liveDataConnection;
|
||||
|
||||
private static boolean isExpeditionListenerRegistered;
|
||||
private static boolean isIgtimiListenerRegistered;
|
||||
|
||||
public WindStatusServlet() {
|
||||
super();
|
||||
isExpeditionListenerRegistered = false;
|
||||
isIgtimiListenerRegistered = false;
|
||||
}
|
||||
|
||||
private void initializeWindReceiver() {
|
||||
if(!isExpeditionListenerRegistered) {
|
||||
isExpeditionListenerRegistered = registerExpeditionListener();
|
||||
lastExpeditionMessages = new ArrayList<WindStatusServlet.ExpeditionMessageInfo>();
|
||||
}
|
||||
|
||||
if (!isIgtimiListenerRegistered) {
|
||||
isIgtimiListenerRegistered = registerIgtimiListener();
|
||||
lastIgtimiMessages = new ArrayList<WindStatusServlet.IgtimiMessageInfo>();
|
||||
igtimiRawMessageCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
initializeWindReceiver();
|
||||
resp.setContentType("text/html");
|
||||
|
||||
PrintWriter out = resp.getWriter();
|
||||
|
||||
out.println("<html>");
|
||||
out.println("<head>");
|
||||
out.println("<title>Wind Status</title>");
|
||||
out.println("<meta http-equiv=refresh content=10>");
|
||||
out.println("</head>");
|
||||
out.println("<body>");
|
||||
out.println("<h3>Igtimi Wind Status ("+igtimiRawMessageCount+" raw messages received)</h3>");
|
||||
if (lastIgtimiMessages != null && lastIgtimiMessages.size() > 0) {
|
||||
int counter = 0;
|
||||
for (ListIterator<IgtimiMessageInfo> iterator = WindStatusServlet.lastIgtimiMessages.listIterator(WindStatusServlet.lastIgtimiMessages.size()); iterator.hasPrevious();) {
|
||||
counter++;
|
||||
IgtimiMessageInfo message = iterator.previous();
|
||||
out.println(message);
|
||||
out.println("<br/>");
|
||||
if (counter >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (igtimiRawMessageCount == 0) {
|
||||
out.println("<i>No Igtimi messages received so far!</i>");
|
||||
} else {
|
||||
out.println("<i>"+igtimiRawMessageCount+" Igtimi message bunch has been received but not enough messages to generate wind information.</i>");
|
||||
}
|
||||
}
|
||||
out.println("<h3>Expedition Wind Status</h3>");
|
||||
if (lastExpeditionMessages.size()>0) {
|
||||
for (ExpeditionMessageInfo message : lastExpeditionMessages) {
|
||||
out.println(message);
|
||||
}
|
||||
} else {
|
||||
out.println("<i>No Expedition messages received so far!</i>");
|
||||
}
|
||||
out.println("</body>");
|
||||
out.println("</html>");
|
||||
out.close();
|
||||
}
|
||||
|
||||
private boolean registerIgtimiListener() {
|
||||
boolean result = false;
|
||||
ServiceTracker<IgtimiConnectionFactory, IgtimiConnectionFactory> igtimiServiceTracker = new ServiceTracker<IgtimiConnectionFactory, IgtimiConnectionFactory>(getContext(), IgtimiConnectionFactory.class, null);
|
||||
igtimiServiceTracker.open();
|
||||
IgtimiConnectionFactory igtimiConnectionFactory = igtimiServiceTracker.getService();
|
||||
for (Account account : igtimiConnectionFactory.getAllAccounts()) {
|
||||
if (account.getUser() != null) {
|
||||
IgtimiConnection igtimiConnection = igtimiConnectionFactory.connect(account);
|
||||
try {
|
||||
liveDataConnection = igtimiConnection.getOrCreateLiveConnection(igtimiConnection.getWindDevices());
|
||||
igtimiWindReceiver = new IgtimiWindReceiver(igtimiConnection.getWindDevices());
|
||||
igtimiWindReceiver.addListener(this);
|
||||
liveDataConnection.addListener(igtimiWindReceiver);
|
||||
liveDataConnection.addListener(this);
|
||||
result = true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean registerExpeditionListener() {
|
||||
boolean result = false;
|
||||
try {
|
||||
ServiceTracker<ExpeditionWindTrackerFactory, ExpeditionWindTrackerFactory> expeditionServiceTracker = new ServiceTracker<ExpeditionWindTrackerFactory, ExpeditionWindTrackerFactory>(
|
||||
getContext(), ExpeditionWindTrackerFactory.class.getName(), null);
|
||||
expeditionServiceTracker.open();
|
||||
UDPExpeditionReceiver receiver = expeditionServiceTracker.getService().getOrCreateWindReceiverOnDefaultPort();
|
||||
receiver.addListener(new ExpeditionListener() {
|
||||
@Override
|
||||
public void received(ExpeditionMessage message) {
|
||||
if(message != null && message.getBoatID() >= 0) {
|
||||
ExpeditionMessageInfo info = new ExpeditionMessageInfo();
|
||||
info.boatID = message.getBoatID();
|
||||
info.message = message;
|
||||
info.messageReceivedAt = new Date();
|
||||
lastExpeditionMessages.add(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
, /*validMessagesOnly*/ false);
|
||||
result = true;
|
||||
} catch (SocketException e) {
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ExpeditionMessageInfo {
|
||||
Integer boatID;
|
||||
ExpeditionMessage message;
|
||||
Date messageReceivedAt;
|
||||
|
||||
public String toString() {
|
||||
if (message.getTrueWind() != null) {
|
||||
return messageReceivedAt.toString() + ": [" + boatID + "] Knots: " + message.getTrueWind().getKnots() + " Bearing: " + message.getTrueWindBearing().getDegrees();
|
||||
}
|
||||
return messageReceivedAt.toString() + ": [" + boatID + "] " + message.getOriginalMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private class IgtimiMessageInfo {
|
||||
Wind wind;
|
||||
private String deviceSerialInfo;
|
||||
|
||||
public IgtimiMessageInfo(Wind wind, String deviceSerialInfo) {
|
||||
this.wind = wind;
|
||||
this.deviceSerialInfo = deviceSerialInfo;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return deviceSerialInfo + ":" + wind.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windDataReceived(Wind wind, String deviceSerialNumber) {
|
||||
lastIgtimiMessages.add(new IgtimiMessageInfo(wind, deviceSerialNumber));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (liveDataConnection != null) {
|
||||
try {
|
||||
liveDataConnection.stop();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
liveDataConnection = null;
|
||||
isIgtimiListenerRegistered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void received(Iterable<Fix> fixes) {
|
||||
igtimiRawMessageCount += 1;
|
||||
}
|
||||
}
|
||||
@@ -103,4 +103,8 @@ After the build, Tycho will start a server instance and runs all tests against t
|
||||
|
||||
Since a full Maven build needs some time, you can also execute the tests in the Eclipse IDE. Here you have to start a server manually via an appropriated launch configuration (e.g. _SailingServer (Proxy, Jetty on 8889)_). From there you can either run all UI-Tests with the JUnit launch configuration _com.sap.sailing.selenium.test_ (with/without proxy) which expects the configuration file under the name _local-test-environment.xml_ or you can run a single test by selecting _Run As -> JUnit Test_ for your test class and specifying the configuration file in the _VM Arguments_ section of the run configuration.
|
||||
|
||||
For a more practical example of how to write page objects and test you should take a look at the [[tutorial|wiki/ui-tests-tutorial]].
|
||||
For a more practical example of how to write page objects and test you should take a look at the [[tutorial|wiki/ui-tests-tutorial]].
|
||||
|
||||
## Updating Selenium
|
||||
|
||||
While our build environment is stable regarding to the used browser version, this may not be the case in your development environment, where you have the newest browser version installed probably. The short release cycles of the browsers often bring changes in the implementation, which are incompatible with Selenium. Therefore you have to update the used Selenium version by performing the following steps to be able to run the tests local.
|
||||
+201
-32
@@ -2,8 +2,12 @@
|
||||
|
||||
In this tutorial, we want to give a practical introduction in how to write UI-Tests with our framework and Selenium. You will learn to prepare your UI for testing as well as to write page objects and tests. For this reason, we write a small test which interacts with the administration console and the goal of the test is to verify the correct creation of a new event.
|
||||
|
||||
## Preparing the UI
|
||||
|
||||
When you start to write UI tests, the first thing you have to do is, to ensure that the UI is testable. This means, that you have to be able to easily find all widgets in the final HTML-Document the user interacts with. The simplest approach here is to use debug identifiers which are provided by GWT and to assign an identifier to all important widgets, like buttons and text fields. Our framework contains a corresponding mechanism to lookup elements by the debug identifier, but more on this later.
|
||||
|
||||
If we look at the "Events" tab of the administration console, we can see that we need the button for adding a new event as well as the table for the validation of the creation (Listing 1).
|
||||
|
||||
public class SailingEventManagementPanel extends SimplePanel implements EventRefresher {
|
||||
public SailingEventManagementPanel(final SailingServiceAsync sailingService,
|
||||
final ErrorReporter errorReporter, final StringMessages stringMessages) {
|
||||
@@ -19,7 +23,7 @@ When you start to write UI tests, the first thing you have to do is, to ensure t
|
||||
}
|
||||
}
|
||||
|
||||
: Listing 1
|
||||
In addition we need all the text fields and the checkbox of the dialog for creating a new event, which are instantiated by the class ´EventCreateDialog`, as well as for the dialog itself (Listing 2).
|
||||
|
||||
public class EventCreateDialog extends EventDialog<EventDTO> {
|
||||
public EventCreateDialog(Collection<EventDTO> existingEvents, StringMessages stringConstants,
|
||||
@@ -27,48 +31,23 @@ When you start to write UI tests, the first thing you have to do is, to ensure t
|
||||
super(new EventParameterValidator(stringConstants, existingEvents), stringConstants, callback);
|
||||
|
||||
nameEntryField = createTextBox(null);
|
||||
nameEntryField.ensureDebugId("NameTextField");
|
||||
nameEntryField.ensureDebugId("NameTextBox");
|
||||
nameEntryField.setWidth("200px");
|
||||
|
||||
venueEntryField = createTextBox(null);
|
||||
venueEntryField.ensureDebugId("VenueTextField");
|
||||
venueEntryField.ensureDebugId("VenueTextBox");
|
||||
venueEntryField.setWidth("200px");
|
||||
|
||||
publicationUrlEntryField = createTextBox(null);
|
||||
publicationUrlEntryField.ensureDebugId("PublicationUrlTextField");
|
||||
publicationUrlEntryField.ensureDebugId("PublicationUrlTextBox");
|
||||
publicationUrlEntryField.setWidth("200px");
|
||||
|
||||
isPublicCheckBox = createCheckbox("");
|
||||
isPublicCheckBox.ensureDebugId("IsPublicCheckbox");
|
||||
isPublicCheckBox.ensureDebugId("IsPublicCheckBox");
|
||||
isPublicCheckBox.setValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
public class EventCreateDialog extends DataEntryDialog<EventDTO> {
|
||||
@Override
|
||||
protected Widget getAdditionalWidget() {
|
||||
...
|
||||
|
||||
Button addCourseAreaButton = new Button(stringConstants.addCourseArea());
|
||||
addCourseAreaButton.ensureDebugId("AddCourseAreaButton");
|
||||
...
|
||||
}
|
||||
|
||||
private Widget createCourseAreaNameWidget(String defaultName, boolean isEnabled) {
|
||||
TextBox textBox = createTextBox(defaultName);
|
||||
textBox.enusreDebugId("CourseArea" + courseAreaNameEntryFields.size() + "TextField");
|
||||
textBox.setVisibleLength(40);
|
||||
textBox.setEnabled(isEnabled);
|
||||
textBox.setWidth("175px");
|
||||
|
||||
courseAreaNameEntryFields.add(textBox);
|
||||
|
||||
return textBox;
|
||||
}
|
||||
}
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
public class SailingEventManagementPanel extends SimplePanel implements EventRefresher {
|
||||
private void openCreateEventDialog() {
|
||||
List<EventDTO> existingEvents = new ArrayList<EventDTO>(eventProvider.getList());
|
||||
@@ -86,4 +65,194 @@ TODO
|
||||
dialog.ensureDebugId("EventCreateDialog");
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Finally you should assign a debug identifier to the event management panel (Listing 3), since it is acts as a context for the search.
|
||||
|
||||
public class AdminConsoleEntryPoint extends AbstractEntryPoint implements RegattaRefresher {
|
||||
protected void doOnModuleLoad() {
|
||||
...
|
||||
|
||||
SailingEventManagementPanel sailingEventManagementPanel = new SailingEventManagementPanel(sailingService,
|
||||
this, stringMessages);
|
||||
sailingEventManagementPanel.ensureDebugId("SailingEventManagementPanel");
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
After you assigned an identifier to all widgets, you are almost done with the preparation. But, there is one more thing you should keep an eye on due the nature of GWT, which heavily use AJAX. If you look at the method `createNewEvent(final EventDTO newEvent)` of the class `SailingEventManagementPanel` as well as `fillEvents()`, which is called by the former one, you see that an `AsyncCallback` is passed to the service methods. In the background GWT creates an asynchrony request here and you don’t know when the request completes. Therefor you can’t tell how long you have to wait, before you can proceed with your test.
|
||||
|
||||
Our framework addresses this by providing a semaphore that counts pending asynchrony requests and the necessary code is automatically injected into the final HTML-Document by the base class `AbstractEntryPoint`. In the case you develop a new entry point, make sure you extend this one.
|
||||
|
||||
To use the semaphore you simply have to replace the `AsyncCallback` with a `MarkedAsyncCallback` and to rename the methods `onFailure` and `onSuccess` to `handleFailure` and `handleSuccess` (Listing 4).
|
||||
|
||||
public class SailingEventManagementPanel extends SimplePanel implements EventRefresher {
|
||||
private void createNewEvent(final EventDTO newEvent) {
|
||||
...
|
||||
sailingService.createEvent(newEvent.getName(), newEvent.venue.getName(), newEvent.publicationUrl,
|
||||
newEvent.isPublic, courseAreaNames, new MarkedAsyncCallback<EventDTO>() {
|
||||
public void handleFailure(Throwable t) {
|
||||
errorReporter.reportError("Error trying to create new event" + newEvent.getName() + ": " +
|
||||
t.getMessage());
|
||||
}
|
||||
|
||||
public void handleSuccess(EventDTO newEvent) {
|
||||
fillEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void fillEvents() {
|
||||
sailingService.getEvents(new MarkedAsyncCallback<List<EventDTO>>() {
|
||||
public void handleFailure(Throwable t) {
|
||||
errorReporter.reportError("Remote Procedure Call getEvents() - Failure: " + t.getMessage());
|
||||
}
|
||||
|
||||
public void handleSuccess(List<EventDTO> result) {
|
||||
allEvents.clear();
|
||||
allEvents.addAll(result);
|
||||
filterTextbox.updateAll(allEvents);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
## Writing the Page Objects
|
||||
|
||||
After finishing the preparation of the UI you can start to write the page objects, which represent complex HTML structures like a table, a form or even the whole page. Page objects hiding this complexity and provide service methods in terms of how the user interacts with the part of the page they represent. They also keep necessary changes in one place, for the case the UI changes. Our framework contains the two base classes `HostPage` and `PageArea`, which provide some common functionality like waiting until all asynchrony requests have completed. For our test we need a page object representing the event management tab as well as a page object for the dialog. We start with the dialog here, since it is need for the event management later.
|
||||
|
||||
To simulate the user interactions like clicking at a button or filling out a text field, you need a reference to the `WebElement`. Selenium uses locating mechanisms, which are define by the different `By` implementations. These are passed to the `findElement` and `findElements` methods of the `WebDriver` or another `WebElement`. While the `WebDriver` searches for the elements in the whole page a `WebElement` only looks up in its children. To support the Page-Object pattern, our framework provides a factory that creates dynamic proxies which are used to initialize annotated instance variables of the type `WebElement` or `List<WebElement>` automatically. The actual element is looked up when you access the variable.
|
||||
|
||||
For the dialog you should define the variables for the 3 different text fields as well as the checkbox and annotated them accordingly with a `FindBy` (Listing 5). The `how` element is used to define the implementation of the `By` and the `using` element the value to use. Since you assigned a debug identifier to all elements, you have to use the implementation `BySeleniumId`. Sometimes it may be necessary that you need another locating mechanism (e.g. `ByXpath`) but for the dialog it is not the case. If you know that an element is static and always there, you can also annotate the field with `CacheLookup`. In this case the element is never looked up again once it has been used the first time. Without this annotation the element is looked up every time it is used.
|
||||
|
||||
public class EventCreateDialogPO extends DataEntryDialogPO {
|
||||
@FindBy(how = BySeleniumId.class, using = "NameTextBox")
|
||||
private WebElement nameTextBox;
|
||||
|
||||
@FindBy(how = BySeleniumId.class, using = "VenueTextBox")
|
||||
private WebElement venueTextBox;
|
||||
|
||||
@FindBy(how = BySeleniumId.class, using = "PublicationUrlTextBox")
|
||||
private WebElement publicationUrlTextBox;
|
||||
|
||||
@FindBy(how = BySeleniumId.class, using = "IsPublicCheckBox")
|
||||
private WebElement isPublicCheckBox;
|
||||
|
||||
public EventCreateDialogPO(WebDriver driver, WebElement element) {
|
||||
super(driver, element);
|
||||
}
|
||||
}
|
||||
|
||||
Now that you have access to the web elements, you can add your service methods to the page object. Usually the service methods are coarse granular, like `login(String user, String password)`, but since the dialog has an error state for the case of invalid input, which you may test later, you should add methods to interact with the single widgets (Listing 6). The methods for clicking the “Ok” or the “Cancel” button are already implemented in the base class `DataEntryDialog`. After finishing the dialog with “Ok”, our base class also waits for all pending asynchrony requests and returns immediately if there is none.
|
||||
|
||||
public class EventCreateDialogPO extends DataEntryDialogPO {
|
||||
...
|
||||
|
||||
public void setName(String name) {
|
||||
this.nameTextBox.clear();
|
||||
this.nameTextBox.sendKeys(name);
|
||||
}
|
||||
|
||||
public void setVenue(String venue) {
|
||||
this.venueTextBox.clear();
|
||||
this.venueTextBox.sendKeys(venue);
|
||||
}
|
||||
|
||||
public void setPublicationUrl(String url) {
|
||||
this.publicationUrlTextBox.clear();
|
||||
this.publicationUrlTextBox.sendKeys(url);
|
||||
}
|
||||
|
||||
public void setPubic(boolean isPublic) {
|
||||
CheckBoxPO checkbox = new CheckBoxPO(this.driver, this.isPublicCheckBox);
|
||||
checkbox.setSelected(isPublic);
|
||||
}
|
||||
}
|
||||
|
||||
The page object for the event management panel is written in a similar fashion (Listing 7). The method `startCreatingEvent` performs a click on the button, looks up the `WebElement` for the dialog programmatically and returns the page object for the dialog. Since service methods should be coarse granular usually, you should also add the method `createEvent` which just takes the data for the new event and hides the interaction with the dialog completely. The last two methods return page objects which represent a GWT CellTable and its entries that already exist in our framework.
|
||||
|
||||
public class SailingEventManagementPanelPO extends PageArea {
|
||||
@FindBy(how = BySeleniumId.class, using = "AddEventButton")
|
||||
private WebElement createEventButton;
|
||||
|
||||
@FindBy(how = BySeleniumId.class, using = "EventsCellTable")
|
||||
private WebElement eventsTable;
|
||||
|
||||
public SailingEventManagementPanelPO(WebDriver driver, WebElement element) {
|
||||
super(driver, element);
|
||||
}
|
||||
|
||||
public EventCreateDialogPO startCreatingEvent() {
|
||||
this.createEventButton.click();
|
||||
|
||||
WebElement dialog = findElementBySeleniumId(this.driver, "EventCreateDialog");
|
||||
|
||||
return new EventCreateDialogPO(this.driver, dialog);
|
||||
}
|
||||
|
||||
public void createEvent(String name, String venue, String url, boolean isPublic) {
|
||||
EventCreateDialogPO dialog = startCreatingEvent();
|
||||
dialog.setName(name);
|
||||
dialog.setVenue(venue);
|
||||
dialog.setPublicationUrl(url);
|
||||
dialog.setPubic(isPublic);
|
||||
dialog.pressOk();
|
||||
}
|
||||
|
||||
public CellTablePO<DataEntryPO> getEventsTable() {
|
||||
return new GenericCellTablePO<DataEntryPO>(this.driver, this.eventsTable, DataEntryPO.class);
|
||||
}
|
||||
|
||||
public List<DataEntryPO> getEvents() {
|
||||
CellTablePO<DataEntryPO> table = getEventsTable();
|
||||
|
||||
return table.getEntries();
|
||||
}
|
||||
}
|
||||
|
||||
In the last step, you have to plug your new page object for the event management in the page object for the administration console, which contains a method to switch to a tab. You should add a new method that calls the existing one with the right parameters and return your page object for the event management (Listing 8).
|
||||
|
||||
public class AdminConsolePagePO extends HostPage {
|
||||
...
|
||||
|
||||
private static final String EVENT_MANAGEMENT_TAB_LABEL = "Events"; //$NON-NLS-1$
|
||||
private static final String EVENT_MANAGEMENT_TAB_IDENTIFIER = "SailingEventManagementPanel"; //$NON-NLS-1$
|
||||
|
||||
...
|
||||
|
||||
public SailingEventManagementPanelPO goToRegattaStructure() {
|
||||
return new SailingEventManagementPanelPO(this.driver, goToTab(EVENT_MANAGEMENT_TAB_LABEL,
|
||||
EVENT_MANAGEMENT_TAB_IDENTIFIER));
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
## Writing the Test
|
||||
|
||||
Now that you have all necessary page objects, you can start to write the test. Our framework uses a specialized JUnit-Runner for the execution of a test with selenium and injects a `TestEnvironment` into a field of the test class that is annotated with `Managed`. The injected `TestEnvironment` gives you access to the `WebDriver` and the base URL to the application under test. However, you can simply extend the base class `AbstractSeleniumTest` which has all required annotations applied already.
|
||||
|
||||
Writing the test is straight forward and there are no big differences to other test cases. You use the page objects to simulate the user interactions and to retrieve the data for your assertions (Listing 9). Since our application persists most of the object, you should clear the state of the `RacingEventService` before each test. For the test itself you use the factory method in the page object for the administration console to navigate to it. There you have to switch to the tab for the event management, where you can create your test event and retrieve the existing events. Since you cleared the state of the application before the test, there should only be one which makes the assertion of the test.
|
||||
|
||||
public class TestEventCreation extends AbstractSeleniumTest {
|
||||
@Before
|
||||
public void clearDatabase() {
|
||||
clearState(getContextRoot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateEvent() {
|
||||
AdminConsolePagePO adminConsole = AdminConsolePagePO.goToPage(getWebDriver(), getContextRoot());
|
||||
SailingEventManagementPanelPO eventManagement = adminConsole.goToEventManagement();
|
||||
|
||||
eventManagement.createEvent("Test Event", "Test Venue", "", false);
|
||||
|
||||
List<DataEntry> events = eventManagement.getEvents();
|
||||
assertTrue(events.size() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
## Executing the Test
|
||||
|
||||
|
||||
## Advanced Topics
|
||||
Reference in new issue
Block a user