Merge remote-tracking branch 'origin/master' into bug5030

This commit is contained in:
Tim Hessenmüller committed 2019-07-18 09:58:27 +02:00
commit cfb8ad2b4e
44 files changed
+1736 -85

No files matched your search

@@ -27,6 +27,10 @@ public abstract class AbstractDistance implements Distance {
return meters > otherMeters ? 1 : meters == otherMeters ? 0 : -1;
}
private Speed inSeconds(double seconds) {
return new KilometersPerHourSpeedImpl(getKilometers() * 3600 / seconds);
}
@Override
public Speed inTime(long milliseconds) {
return new KilometersPerHourSpeedImpl(getKilometers() * 1000. * 3600. / milliseconds);
@@ -34,7 +38,7 @@ public abstract class AbstractDistance implements Distance {
@Override
public Speed inTime(Duration duration) {
return inTime(duration.asMillis());
return inSeconds(duration.asSeconds());
}
@Override
@@ -13,6 +13,7 @@ import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsDurationImpl;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.common.impl.SecondsDurationImpl;
public class DurationTest {
@Test
@@ -88,7 +89,7 @@ public class DurationTest {
}
@Test
public void testOperations() {
public void testOperationsMilliseconds() {
Duration someSeconds = new MillisecondsDurationImpl(2400).minus(1000);
assertEquals(0, someSeconds.asDays(), 0.1);
assertEquals(0, someSeconds.asHours(), 0.1);
@@ -102,6 +103,22 @@ public class DurationTest {
assertEquals(3600*25, oneDay.asSeconds(), 0.1);
assertEquals(3600*25*1000, oneDay.asMillis());
}
@Test
public void testOperationsSeconds() {
Duration someSeconds = new SecondsDurationImpl(2.4).minus(1000);
assertEquals(0, someSeconds.asDays(), 0.1);
assertEquals(0, someSeconds.asHours(), 0.1);
assertEquals(0, someSeconds.asMinutes(), 0.1);
assertEquals(1.4, someSeconds.asSeconds(), 0.1);
assertEquals(1400, someSeconds.asMillis(), 0.1);
Duration oneDay = Duration.ONE_DAY.plus(Duration.ONE_HOUR);
assertEquals(25, oneDay.asHours(), 0.001);
assertEquals(1, oneDay.asDays(), 0.01);
assertEquals(25*60, oneDay.asMinutes(), 0.1);
assertEquals(3600*25, oneDay.asSeconds(), 0.1);
assertEquals(3600*25*1000, oneDay.asMillis());
}
@Test
public void compare() {
@@ -4,6 +4,14 @@
<div id="mainContent">
<h4 class="articleHeadline">What's New - SAP Sailing Race Manager</h4>
<div class="innerContent">
<h5 class="articleSubheadline">July 2019</h5>
<ul class="bulletList">
<li>The app no longer remembers the last entered duration when setting a protest time.
The protest duration can now be configured correctly in the app settings.
Please consider that a protest duration in the specific regatta configuration is preferred to
the protest duration in the default regatta configuration.</li>
</ul>
<h5 class="articleSubheadline">May 2019</h5>
<ul class="bulletList">
<li>The Course editor now works correctly when managing more than one regatta.
@@ -357,7 +357,14 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint
ResultImportUrlsManagementPanel resultImportUrlsManagementPanel = new ResultImportUrlsManagementPanel(
getSailingService(), getUserService(), this, getStringMessages());
panel.addToTabPanel(connectorsTabPanel, new DefaultRefreshableAdminConsolePanel<ResultImportUrlsManagementPanel>(resultImportUrlsManagementPanel),
panel.addToTabPanel(connectorsTabPanel,
new DefaultRefreshableAdminConsolePanel<ResultImportUrlsManagementPanel>(
resultImportUrlsManagementPanel) {
@Override
public void refreshAfterBecomingVisible() {
resultImportUrlsManagementPanel.refreshUrlList();
}
},
getStringMessages().resultImportUrls(),
SecuredDomainType.RESULT_IMPORT_URL.getPermission(DefaultActions.values()));
StructureImportManagementPanel structureImportUrlsManagementPanel = new StructureImportManagementPanel(
@@ -107,7 +107,7 @@ public class ResultImportUrlsManagementPanel extends FlowPanel {
return result;
}
private void refreshUrlList() {
public void refreshUrlList() {
String selectedProviderName = getSelectedProviderName();
if (selectedProviderName != null) {
sailingService.getResultImportUrls(selectedProviderName, new AsyncCallback<List<String>>() {
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Tiempo total de ceñida recorrido en esta carrera
noCustomGrouperScriptTextError=El script de grupo está vacío
noGrouperSelectedError=No se ha seleccionado ningún tipo de agrupador
dataMining=Minería de datos
changeOrientation=Modificar orientación
hideToolbar=Ocultar barra de herramientas
showSeriesLeaderboards=Mostrar tablas de clasificación de las series
showOverallLeaderboard=Mostrar tabla de clasificación general
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Se ha importado el número siguiente de correcciones
timeToStart={0} hasta la salida
timeSinceStart={0} desde la salida
distanceToLine=Distancia hasta la línea de salida
TWATooltip=Ángulo entre la dirección de los competidores y el viento
TWATooltip=El ángulo entre el rumbo de los competidores sobre el fondo y el viento, muestra valores negativos que implican viento de babor, valores positivos que implican viento de estribor.
absoluteTWATooltip=El ángulo positivo entre el rumbo de los competidores sobre el fondo y el viento, sin signo +/-
TWA=Ángulo real del viento
absoluteTWA=Ángulo real absoluto del viento
showBoatClassChartsLabel=También puede visualizar el diagrama global para las clases de embarcación disponibles.
showDiagram=Mostrar diagrama
rerunQueryAfterRefreshTooltip=Vuelve a ejecutar la consulta después de que se hayan refrescado las tablas.
@@ -1927,6 +1930,7 @@ finishingTime=Hora de llegada
finishTimeString=Hora de fin
setFinishingAndFinishTime=Fijar hora de llegada y de fin
finishTimeMustBeAtOrAfterFinishingTime=Hora de fin debe ser a la hora de llegada o posterior
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Debe indicar la hora de llegada si indica la hora de fin
boatNameMustNotBeEmpty=Proporcione un nombre de embarcación que no esté en blanco
errorUpdatingExpeditionDeviceConfiguration=Error al actualizar la configuración del dispositivo con la expedición "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=¿Desea realmente eliminar la configuración del dispositivo con la expedición "{0}"?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Tratar conexión SwissTiming
filterSwissTimingConnections=Filtrar conexiones SwissTiming
addSwissTimingConnection=Añadir conexión SwissTiming
updateURL=Actualizar URL
startProcedure=Iniciar procedimiento
startMode=Iniciar modo
gateOpeningTime=Horario de apertura de puerta
protestTimeInMinutes=Tiempo para protestar en minutos
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Durée totale de navigation dans le lit du vent dans cette
noCustomGrouperScriptTextError=Script de regroupement vide
noGrouperSelectedError=Aucun type de regroupement sélectionné
dataMining=Exploration de données
changeOrientation=Modifier orientation
hideToolbar=Masquer barre d''outils
showSeriesLeaderboards=Afficher palmarès de la série
showOverallLeaderboard=Afficher palmarès général
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Le nombre suivant de points vent ont été importés
timeToStart={0} jusqu''au départ
timeSinceStart={0} depuis le départ
distanceToLine=Distance jusqu''à la ligne de départ
TWATooltip=L''angle entre la direction du concurrent et le vent.
TWATooltip=L''angle entre la route fond des concurrents et le vent, accompagné d''un signe : les valeurs négatives indiquent que le vent vient de bâbord, les valeurs positives indiquent que le vent vient de tribord.
absoluteTWATooltip=L''angle positif entre la route fond des concurrents et le vent, sans signe +/-.
TWA=Angle du vent réel
absoluteTWA=Angle du vent réel absolu
showBoatClassChartsLabel=Vous pouvez également afficher le diagramme global pour les catégories de bateau disponibles.
showDiagram=Afficher diagramme
rerunQueryAfterRefreshTooltip=Réexécute la requête après l''actualisation des tables.
@@ -1927,6 +1930,7 @@ finishingTime=Heure d''arrivée du premier
finishTimeString=Heure de fermeture de la ligne d''arrivée
setFinishingAndFinishTime=Définir heure d''arrivée du premier et de fermeture de la ligne d''arrivée
finishTimeMustBeAtOrAfterFinishingTime=L''heure de fermeture de la ligne d''arrivée doit être égale ou ultérieure à l''heure d''arrivée du premier.
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Vous devez saisir une heure d''arrivée du premier si vous saisissez une heure de fermeture de la ligne d''arrivée.
boatNameMustNotBeEmpty=Veuillez indiquer un nom de bateau.
errorUpdatingExpeditionDeviceConfiguration=Erreur lors de la mise à jour de la configuration du terminal Expedition "{0}" : {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Voulez-vous vraiment supprimer la configuration du terminal Expedition "{0}" ?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Modifier connexion SwissTiming
filterSwissTimingConnections=Filtrer connexions SwissTiming
addSwissTimingConnection=Ajouter connexion SwissTiming
updateURL=Mettre à jour URL
startProcedure=Procédure de départ
startMode=Mode de départ
gateOpeningTime=Heure d''ouverture de la porte
protestTimeInMinutes=Durée de réclamation en minutes
@@ -339,7 +339,7 @@ notAvailable=Non disponibile
noGroupSelected=Nessun gruppo selezionato
combinedWindSourceTypeName=Combinato
legMiddleWindSourceTypeName=Metà della tratta
courseBasedWindSourceTypeName=Progettazione rotta
courseBasedWindSourceTypeName=Progettazione della rotta
trackBasedEstimationWindSourceTypeName=Stima
windSensorWindSourceTypeName=Misurato
webWindSourceTypeName=Manuale
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Tempo totale di navigazione di bolina in questa gara
noCustomGrouperScriptTextError=Script membro vuoto
noGrouperSelectedError=Nessun tipo di membro selezionato
dataMining=Estrazione dati
changeOrientation=Modifica orientamento
hideToolbar=Nascondi barra degli strumenti
showSeriesLeaderboards=Mostra classifiche serie
showOverallLeaderboard=Mostra classifica generale
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Per le gare elencate è stato importato il seguente n
timeToStart={0} all’inizio
timeSinceStart={0} dall’inizio
distanceToLine=Distanza fino alla linea di partenza
TWATooltip=L’angolo tra la direzione dei concorrenti e il vento
TWATooltip=L’angolo tra la rotta effettiva al fondo dei concorrenti e il vento tracciato, dove i valori negativi indicano il vento da sinistra e quelli positivi il vento da destra
absoluteTWATooltip=L''angolo positivo tra la rotta effettiva al fondo dei concorrenti e il vento, senza segno positivo o negativo
TWA=Angolo del vento reale
absoluteTWA=Angolo assoluto del vento reale
showBoatClassChartsLabel=È anche possibile visualizzare il diagramma complessivo delle classi di barca disponibili.
showDiagram=Mostra diagramma
rerunQueryAfterRefreshTooltip=Riesegue la query dopo che le tabelle sono state aggiornate.
@@ -1927,6 +1930,7 @@ finishingTime=Ora conclusione
finishTimeString=Ora di fine
setFinishingAndFinishTime=Imposta ora conclusione e ora di fine
finishTimeMustBeAtOrAfterFinishingTime=L’ora di fine deve essere uguale o successiva all’ora di conclusione
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Se si inserisce un''ora di fine occorre indicare un''ora di conclusione
boatNameMustNotBeEmpty=Fornisci un nome barca non vuoto
errorUpdatingExpeditionDeviceConfiguration=Errore nell’aggiornamento della configurazione del dispositivo Expedition "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Confermare l''eliminazione della configurazione del dispositivo Expedition "{0}"?
@@ -2001,9 +2005,9 @@ raceNameIsRequired=Deve essere indicato un nome per la gara suddivisa.
raceNameIsAlreadyUsed=Il nome indicato è già in uso
errorWhilePreparingToSliceARace=Errore nella preparazione della suddivisione gara
errorWhileSlicingARace=Errore nella suddivisione della gara
windFinderWindSourceTypeName=
windFinderWindSourceTypeName=Windfinder
windFinderWindSourceTypeTooltip=Vento misurato da uno o più punti di www.windfinder.com
windFinder=
windFinder=Windfinder
enterTagsForTheVideo=Inserisci i tag per il video
enterIdOfWindFinderReviewedSpotCollection=Inserisci l''ID di una collezione di punti WindFinder rivista, es. "schilksee"
windFinderSpotCollectionsList=Collezioni punti WindFinder
@@ -2128,7 +2132,7 @@ automaticResizeNeedsStorageService=Per il ridimensionamento automatico occorre u
qrCodeBoatInviteTitle=Sei invitato a tracciare la barca {0} per la regata {1}
qrCodeMarkInviteTitle=Sei invitato a tracciare il segnale {0} per la regata {1}
qrCodeCompetitorInviteTitle=Benvenuto {0} alla regata {1}
qrCodeTitleBouy=Benvenuto come {0}
qrCodeTitleBouy=Benvenuto come posaboe per la regata {0}
qrCodeTitle=Benvenuto alla regata {0}
qrCodeSubtitle={0} - {1}
qrCodeTitleOpenRegatta=Benvenuto alla regata pubblica {0}
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Modifica connessione SwissTiming
filterSwissTimingConnections=Filtra connessioni SwissTiming
addSwissTimingConnection=Aggiungi connessione SwissTiming
updateURL=Aggiorna URL
startProcedure=Procedura di partenza
startMode=Modalità di partenza
gateOpeningTime=Ora di apertura del cancello
protestTimeInMinutes=Tempo per protesta (minuti)
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=このレースでアップウィンドで移動した合
noCustomGrouperScriptTextError=分類スクリプトが空です
noGrouperSelectedError=分類タイプが選択されていません
dataMining=データマイニング
changeOrientation=方向の変更
hideToolbar=ツールバー非表示
showSeriesLeaderboards=シリーズリーダーボード表示
showOverallLeaderboard=全体リーダーボード表示
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=一覧されたレースに対して次の数のフ
timeToStart=スタートまで {0}
timeSinceStart=スタートから {0}
distanceToLine=スタートラインまでの距離
TWATooltip=競技者の方向と風の間のアングル
TWATooltip=競技者の対地コースと風の間のアングル (符号あり、マイナスの値は左舷からの風、プラスの値は右舷からの風を意味)
absoluteTWATooltip=競技者の対地コースと風の間のアングル (+/- 符号のないプラスの値)
TWA=真の風角度
absoluteTWA=絶対真風角度
showBoatClassChartsLabel=利用可能な艇種の全体図も表示することができます。
showDiagram=ダイアグラム表示
rerunQueryAfterRefreshTooltip=テーブルがリフレッシュされた後にクエリを再実行します。
@@ -1927,6 +1930,7 @@ finishingTime=フィニッシュ予定時刻
finishTimeString=フィニッシュ時刻
setFinishingAndFinishTime=フィニッシュ予定時刻とフィニッシュ時刻の設定
finishTimeMustBeAtOrAfterFinishingTime=フィニッシュ時刻はフィニッシュ予定時刻以降であることが必要
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=フィニッシュ時刻を入力する場合、フィニッシュ予定時刻を入力することが必要
boatNameMustNotBeEmpty=空でない艇名を指定してください
errorUpdatingExpeditionDeviceConfiguration=Expedition デバイス設定 "{0}" の更新でエラーが発生: {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Expedition デバイス設定 "{0}" を削除しますか
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=SwissTiming 接続の編集
filterSwissTimingConnections=SwissTiming 接続のフィルタリング
addSwissTimingConnection=SwissTiming 接続の追加
updateURL=URL 更新
startProcedure=スタート手順
startMode=スタートモード
gateOpeningTime=ゲートオープニング時刻
protestTimeInMinutes=抗議受付時間 (分)
@@ -807,7 +807,7 @@ explainNoConnectionsFromReplicas=Nenhuma conexão de uma réplica encontrada. Vo
stopConnectionToMaster=Parar replicação
asLink=Como link
noStarttimeAnnouncedYet=Pendente
raceIsRunning=Em execução
raceIsRunning=Em competição
raceIsFinishing=Concluindo
raceIsFinished=Concluído
raceIsScheduled=Planejado
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Tempo total navegado a barlavento nesta corrida
noCustomGrouperScriptTextError=O script do sistema de agrupamento está vazio
noGrouperSelectedError=Nenhum tipo de sistema de agrupamento selecionado
dataMining=Data mining
changeOrientation=Modificar orientação
hideToolbar=Ocultar barra de ferramentas
showSeriesLeaderboards=Visualizar painéis de classificação da série
showOverallLeaderboard=Visualizar painel de classificação geral
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=O número seguinte de pontos fixos foi importado para
timeToStart={0} até partida
timeSinceStart={0} desde partida
distanceToLine=Distância para linha de partida
TWATooltip=O ângulo entre a direção dos competidores e o vento
TWATooltip=O ângulo entre o percurso no fundo dos competidores e o vento, assinalado, sendo os valores negativos vento de bombordo e os valores positivos vento de boreste
absoluteTWATooltip=O ângulo positivo entre o percurso no fundo dos competidores e o vento, sem sinal positivo/negativo
TWA=Ângulo do vento verdadeiro
absoluteTWA=Ângulo do vento verdadeiro absoluto
showBoatClassChartsLabel=Você também pode ver o diagrama geral para as classes de barcos disponíveis.
showDiagram=Visualizar diagrama
rerunQueryAfterRefreshTooltip=Executa novamente a consulta após a atualização das tabelas.
@@ -1775,7 +1778,7 @@ couldNotSaveCompetitorRegistrations=Não foi possível gravar registros de compe
searchTagForImportedCompetitorPlaceholder=Utilizar, por exemplo, corrida + nome da flotilha
version=Versão: {0}
replicables=Replicando objetos
set=Definir
set=Curso
eventRegattaRacesLegendWind=# sensores de vento
eventRegattaRacesLegendVideo=# fluxos de vídeo
eventRegattaRacesLegendAudio=# fluxos de áudio
@@ -1927,6 +1930,7 @@ finishingTime=Hora de chegada
finishTimeString=Hora de conclusão
setFinishingAndFinishTime=Definir hora de chegada e conclusão
finishTimeMustBeAtOrAfterFinishingTime=A hora de conclusão deve ser igual ou posterior à hora de chegada
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Você precisa inserir uma hora de chegada se inserir uma hora de conclusão
boatNameMustNotBeEmpty=Forneça um nome de barco não vazio
errorUpdatingExpeditionDeviceConfiguration=Erro ao atualizar configuração de dispositivo Expedition "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Você quer realmente remover a configuração do dispositivo Expedition "{0}"?
@@ -2114,7 +2118,7 @@ gender=Sexo
manage2SailEventIdBox=ID de evento Manage2Sail
manage2SailEventURLBox=URL de evento Manage2Sail (json)
manage2SailEventIdBoxTooltip=ID de evento ou um URL que contenha o ID de evento do Manage2Sail
manage2SailPort=Porta
manage2SailPort=Bombordo
audioFiles=Faixas de áudio
selectMedia=Selecionar mídia
swissTimingUpdateURL=Atualizar URL (por exemplo, para hora de partida e feedback de mudança de percurso)
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Processar conexão SwissTiming
filterSwissTimingConnections=Filtrar conexões SwissTiming
addSwissTimingConnection=Adicionar conexão SwissTiming
updateURL=Atualizar URL
startProcedure=Procedimento de partida
startMode=Modo de partida
gateOpeningTime=Hora de abertura do portão
protestTimeInMinutes=Tempo de protesto em minutos
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Общее время хода против ветра в
noCustomGrouperScriptTextError=Скрипт группирования пуст
noGrouperSelectedError=Не выбран тип группирования
dataMining=Добыча данных
changeOrientation=Изменить ориентацию
hideToolbar=Скрыть панель инструментов
showSeriesLeaderboards=Показать таблицы лидеров серии
showOverallLeaderboard=Показать итоговую таблицу лидеров
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Для перечисленных гонок импо
timeToStart={0} до старта
timeSinceStart={0} от старта
distanceToLine=Дистанция до линии старта
TWATooltip=Угол между направлением участников и ветром
TWATooltip=Угол между курсом участников относительно грунта и ветром, со знаком, отрицательные значения означают ветер по левому борту, положительные - ветер по правому борту.
absoluteTWATooltip=участников относительно грунта и ветром, без знака +/-
TWA=Угол истинного ветра
absoluteTWA=Абсолютный угол истинного ветра
showBoatClassChartsLabel=Также можно просмотреть общую диаграмму для доступных классов лодок.
showDiagram=Показать диаграмму
rerunQueryAfterRefreshTooltip=Повторно выполняет запрос после обновления таблиц.
@@ -1927,6 +1930,7 @@ finishingTime=Время финиширования
finishTimeString=Время финиша
setFinishingAndFinishTime=Указать время финиширования и финиша
finishTimeMustBeAtOrAfterFinishingTime=Время финиша должно совпадать или быть после времени финиширования
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=При вводе времени финиша необходимо ввести время финиширования
boatNameMustNotBeEmpty=Укажите непустое имя лодки
errorUpdatingExpeditionDeviceConfiguration=Ошибка при обновлении конфигурации устройства Expedition "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Действительно удалить конфигурацию устройства Expedition "{0}"?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Редактировать соединение Swiss
filterSwissTimingConnections=Фильтр соединений SwissTiming
addSwissTimingConnection=Добавить соединение SwissTiming
updateURL=Обновить URL
startProcedure=Процедура старта
startMode=Режим старта
gateOpeningTime=Время открытия ворот
protestTimeInMinutes=Времени протеста в минутах
@@ -696,9 +696,9 @@ speedOverGroundFiveSecondsBeforeStartTooltip=起航前五秒时的实际速度
speedOverGroundWhenPassingStartTooltip=参赛队越过起航线时的实际速度
startTack=开始迎风转向
startTackTooltip=参赛队开始迎风转向
portTack=端口
portTack=左舷
starboardTack=右舷
portSide=端口
portSide=左舷
starboardSide=右舷
pleaseEnterANumber=请输入数字。
competitorsFilter=参赛队筛选器
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=本轮次比赛迎风航行的总时间
noCustomGrouperScriptTextError=群集器脚本为空
noGrouperSelectedError=未选择群集器类型
dataMining=数据挖掘
changeOrientation=更改方向
hideToolbar=隐藏工具栏
showSeriesLeaderboards=显示系列积分榜
showOverallLeaderboard=显示总积分榜
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=已为所列的比赛轮次导入以下修复: {0}
timeToStart={0} - 开始
timeSinceStart={0} - 开始后
distanceToLine=与起航线的距离
TWATooltip=赛队航行的方向和风的夹角
TWATooltip=参赛队对地航向和风的夹角,设有符号标示,负值表示风来自左舷,正值表示风来自右舷
absoluteTWATooltip=参赛队对地航向和风的正向夹角,不带 +/- 符号
TWA=实际风向角
absoluteTWA=绝对实际风向角
showBoatClassChartsLabel=还可以查看可用船只级别的总直直方图。
showDiagram=显示直方图
rerunQueryAfterRefreshTooltip=在刷新表后,重新运行查询。
@@ -1927,6 +1930,7 @@ finishingTime=结束时间
finishTimeString=终点线时间
setFinishingAndFinishTime=设置结束和终点线时间
finishTimeMustBeAtOrAfterFinishingTime=终点线时间必须与结束时间相同或在其之后
youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=如果输入终点线时间,则必须输入结束时间。
boatNameMustNotBeEmpty=请提供非空船只名称
errorUpdatingExpeditionDeviceConfiguration=更新导航图设备配置 "{0}" 出错:{1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=是否确定移除导航图设备配置 "{0}"?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=编辑 SwissTiming 连接
filterSwissTimingConnections=筛选 SwissTiming 连接
addSwissTimingConnection=添加 SwissTiming 连接
updateURL=更新 URL
startProcedure=起航程序
startMode=起航模式
gateOpeningTime=门标开启时间
protestTimeInMinutes=声明时间(分钟)
@@ -5209,19 +5209,13 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet
@Override
public List<String> getResultImportUrls(String resultProviderName) {
List<String> result = new ArrayList<String>();
final List<String> result = new ArrayList<String>();
ResultUrlProvider urlBasedScoreCorrectionProvider = getUrlBasedScoreCorrectionProvider(resultProviderName);
ResultUrlRegistry resultUrlRegistry = getResultUrlRegistry();
if (urlBasedScoreCorrectionProvider != null) {
Iterable<URL> allUrls = resultUrlRegistry.getResultUrls(resultProviderName);
final Subject subject = SecurityUtils.getSubject();
for (URL url : allUrls) {
if (subject
.isPermitted(SecuredDomainType.RESULT_IMPORT_URL.getStringPermissionForTypeRelativeIdentifier(
DefaultActions.READ, new TypeRelativeObjectIdentifier(
urlBasedScoreCorrectionProvider.getName(), url.toString())))) {
result.add(url.toString());
}
Iterable<URL> allUrlsReadableBySubject = resultUrlRegistry.getResultUrls(resultProviderName);
for (URL url : allUrlsReadableBySubject) {
result.add(url.toString());
}
}
return result;
@@ -12,8 +12,12 @@ Export-Package: com.sap.sailing.resultimport,
com.sap.sailing.resultimport.impl
Import-Package: com.sap.sailing.competitorimport,
com.sap.sailing.domain.common,
com.sap.sailing.domain.common.security,
com.sap.sailing.domain.persistence,
com.sap.sailing.server.interfaces,
com.sap.sse.security.shared,
org.apache.shiro;version="1.2.2",
org.apache.shiro.subject;version="1.2.2",
org.osgi.framework;version="1.5.0",
org.osgi.util.tracker;version="1.5.1"
Require-Bundle: com.sap.sse.common
@@ -2,14 +2,43 @@ package com.sap.sailing.resultimport;
import java.net.URL;
import org.apache.shiro.subject.Subject;
import com.sap.sailing.domain.common.security.SecuredDomainType;
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
/**
* A registry for result URL's of score correction providers
* @author Frank
*/
public interface ResultUrlRegistry {
/**
* Registers the URL {@code url} for the result provider named {@code resultProviderName}. Unlike the
* {@link #getResultUrls(String)} method, here no security check is performed. The caller is expected to assert that
* the calling {@link Subject} has the permission {@link SecuredDomainType#RESULT_IMPORT_URL
* RESULT_IMPORT_URL}.{@link DefaultActions#CREATE CREATE} after setting the ownership. Implementing this
* check properly is done best with the {@code SecurityService} which is not visible from this bundle.
*/
void registerResultUrl(String resultProviderName, URL url);
/**
* Registers the URL {@code url} from the result provider named {@code resultProviderName}. Unlike the
* {@link #getResultUrls(String)} method, here no security check is performed. The caller is expected to assert that
* the calling {@link Subject} has the permission {@link SecuredDomainType#RESULT_IMPORT_URL
* RESULT_IMPORT_URL}.{@link DefaultActions#DELETE DELETE} after setting the ownership. Implementing this
* check properly is done best with the {@code SecurityService} which is not visible from this bundle.
*/
void unregisterResultUrl(String resultProviderName, URL url);
/**
* Obtains the result import URLs registered for the provider named {@code resultProviderName}. If no such URLs are
* found, an empty iterable is returned. The URLs returned are filtered by the calling {@link Subject}'s permission
* to The URLs returned are filtered by the calling {@link Subject}'s permission
* to {@link SecuredDomainType#RESULT_IMPORT_URL RESULT_IMPORT_URL}.{@link DefaultActions#READ READ} them. them.
*
* @return an always valid, non-{@code null} iterable
*/
Iterable<URL> getResultUrls(String resultProviderName);
Iterable<String> getResultProviderNames();
}
@@ -1,14 +1,22 @@
package com.sap.sailing.resultimport.impl;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import com.sap.sailing.domain.common.security.SecuredDomainType;
import com.sap.sailing.domain.persistence.DomainObjectFactory;
import com.sap.sailing.domain.persistence.MongoObjectFactory;
import com.sap.sailing.resultimport.ResultUrlRegistry;
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
import com.sap.sse.security.shared.TypeRelativeObjectIdentifier;
public class ResultUrlRegistryImpl implements ResultUrlRegistry {
private final Map<String, Set<URL>> resultUrls;
@@ -41,9 +49,18 @@ public class ResultUrlRegistryImpl implements ResultUrlRegistry {
@Override
public Iterable<URL> getResultUrls(String resultProviderName) {
Iterable<URL> result;
if(resultUrls.containsKey(resultProviderName)) {
result = resultUrls.get(resultProviderName);
final Subject subject = SecurityUtils.getSubject();
final Iterable<URL> result;
if (resultUrls.containsKey(resultProviderName)) {
result = new ArrayList<>(resultUrls.get(resultProviderName));
for (final Iterator<URL> urlIterator = result.iterator(); urlIterator.hasNext();) {
final URL url = urlIterator.next();
if (!subject.isPermitted(SecuredDomainType.RESULT_IMPORT_URL
.getStringPermissionForTypeRelativeIdentifier(DefaultActions.READ,
new TypeRelativeObjectIdentifier(resultProviderName, url.toString())))) {
urlIterator.remove();
}
}
} else {
result = Collections.emptySet();
}
@@ -0,0 +1,172 @@
package com.sap.sailing.selenium.api.event;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import com.sap.sailing.selenium.api.core.ApiContext;
import com.sap.sailing.selenium.api.core.JsonWrapper;
import com.sap.sse.common.Util.Triple;
public class TrackedEventsApi {
private static final String TRACKED_EVENTS_URL = "/api/v1/trackedevents/";
private static final String KEY_TRACKED_EVENTS = "trackedEvents";
private static final String KEY_LEADERBOARD_NAME = "leaderboardName";
private static final String KEY_EVENT_ID = "eventId";
private static final String KEY_EVENT_IS_ARCHIVED = "isArchived";
private static final String KEY_EVENT_NAME = "name";
private static final String KEY_EVENT_START = "start";
private static final String KEY_EVENT_END = "end";
private static final String KEY_EVENT_BASE_URL = "url";
private static final String KEY_EVENT_IS_OWNER = "isOwner";
private static final String KEY_EVENT_REGATTA_SECRET = "regattaSecret";
private static final String KEY_EVENT_TRACKED_ELEMENTS = "trackedElements";
private static final String KEY_TRACKED_ELEMENT_DEVICE_ID = "deviceId";
private static final String KEY_TRACKED_ELEMENT_COMPETITOR_ID = "competitorId";
private static final String KEY_TRACKED_ELEMENT_BOAT_ID = "boatId";
private static final String KEY_TRACKED_ELEMENT_MARK_ID = "markId";
private static final String KEY_QUERY_INCLUDE_ARCHIVED = "includeArchived";
public TrackedEvents getTrackedEvents(ApiContext ctx, boolean isArchived) {
final Map<String, String> queryParams = new HashMap<>();
queryParams.put(KEY_QUERY_INCLUDE_ARCHIVED, Boolean.toString(isArchived));
return new TrackedEvents(ctx.get(TRACKED_EVENTS_URL, queryParams));
}
public void updateOrCreateTrackedEvent(ApiContext ctx, String eventId, String leaderboardName, String eventBaseUrl,
String deviceId, Set<Triple<String, String, String>> trackedIds, String regattaSecret) {
final JSONObject json = new JSONObject();
json.put(KEY_EVENT_BASE_URL, eventBaseUrl);
final JSONArray elements = new JSONArray();
for (Triple<String, String, String> trackedId : trackedIds) {
final JSONObject elem = new JSONObject();
elem.put(KEY_TRACKED_ELEMENT_DEVICE_ID, deviceId);
if (trackedId.getA() != null) {
elem.put(KEY_TRACKED_ELEMENT_COMPETITOR_ID, trackedId.getA());
} else if (trackedId.getB() != null) {
elem.put(KEY_TRACKED_ELEMENT_BOAT_ID, trackedId.getB());
} else if (trackedId.getC() != null) {
elem.put(KEY_TRACKED_ELEMENT_MARK_ID, trackedId.getC());
}
elements.add(elem);
}
json.put(KEY_EVENT_REGATTA_SECRET, regattaSecret);
json.put(KEY_EVENT_TRACKED_ELEMENTS, elements);
json.put(KEY_EVENT_IS_ARCHIVED, false);
ctx.put(TRACKED_EVENTS_URL + eventId + "/" + leaderboardName, new HashMap<>(), json);
}
public void setArchived(ApiContext ctx, String eventId, String leaderboardName, boolean archived) {
final HashMap<String, String> formParams = new HashMap<>();
formParams.put(KEY_QUERY_INCLUDE_ARCHIVED, Boolean.toString(archived));
ctx.post(TRACKED_EVENTS_URL + eventId + "/" + leaderboardName, new HashMap<String, String>(), formParams);
}
public void deleteEventTrackings(ApiContext ctx, String eventId, String leaderboardName) {
ctx.delete(TRACKED_EVENTS_URL + eventId + "/" + leaderboardName);
}
public class TrackedElement extends JsonWrapper {
public TrackedElement(JSONObject json) {
super(json);
}
public String getCompetitorId() {
return get(KEY_TRACKED_ELEMENT_COMPETITOR_ID);
}
public String getBoatId() {
return get(KEY_TRACKED_ELEMENT_BOAT_ID);
}
public String getMarkId() {
return get(KEY_TRACKED_ELEMENT_MARK_ID);
}
public String getDeviceId() {
return get(KEY_TRACKED_ELEMENT_DEVICE_ID);
}
}
public class TrackedEvents extends JsonWrapper {
public TrackedEvents(JSONObject json) {
super(json);
}
public Iterable<TrackedEvent> getEvents() {
final Collection<TrackedEvent> events = new ArrayList<>();
final JSONArray array = get(KEY_TRACKED_EVENTS);
for (final Object json : array) {
events.add(new TrackedEvent((JSONObject) json));
}
return events;
}
}
public class TrackedEvent extends JsonWrapper {
public TrackedEvent(JSONObject json) {
super(json);
}
public String getEventId() {
return get(KEY_EVENT_ID);
}
public String getEventName() {
return get(KEY_EVENT_NAME);
}
public String getEventStart() {
return get(KEY_EVENT_START);
}
public String getEventEnd() {
return get(KEY_EVENT_END);
}
public String getEventBaseUrl() {
return get(KEY_EVENT_BASE_URL);
}
public boolean isArchived() {
return get(KEY_EVENT_IS_ARCHIVED);
}
public boolean isOwner() {
return get(KEY_EVENT_IS_OWNER);
}
public Iterable<TrackedElement> getTrackedElements() {
final JSONArray array = get(KEY_EVENT_TRACKED_ELEMENTS);
final Collection<TrackedElement> col = new ArrayList<>();
if (array != null) {
for (final Object w : array) {
col.add(new TrackedElement((JSONObject) w));
}
}
return col;
}
public String getLeaderboardName() {
return get(KEY_LEADERBOARD_NAME);
}
public String getRegattaSecret() {
return get(KEY_EVENT_REGATTA_SECRET);
}
}
}
@@ -14,7 +14,9 @@ public class RegattaApi {
private static final String REGATTAS = "/api/v1/regattas";
private static final String LIST_REGATTA_RACES = "/races";
private static final String COMPETITOR_CREATE_AND_ADD_WITH_BOAT = "/competitors/createandadd";
private static final String COMPETITORS = "/competitors";
private static final String COMPETITOR_CREATE_AND_ADD_WITH_BOAT = COMPETITORS + "/createandadd";
private static final String ADD = "/add";
private static final String ADD_RACE_COLUMN_URL = "/addracecolumns";
private static final String LIST_COMPETITORS = "/competitors";
@@ -87,5 +89,12 @@ public class RegattaApi {
}
return result;
}
public void addCompetitor(ApiContext ctx, String regattaName, UUID competitorId, Optional<String> secret) {
final String url = REGATTAS + "/" + regattaName + COMPETITORS + "/" + competitorId + ADD;
final Map<String, String> queryParams = new TreeMap<>();
queryParams.put("secret", secret.orElse(null));
ctx.post(url, queryParams);
}
}
@@ -3,39 +3,54 @@ package com.sap.sailing.selenium.api.test;
import static com.sap.sailing.domain.common.CompetitorRegistrationType.CLOSED;
import static com.sap.sailing.selenium.api.core.ApiContext.SERVER_CONTEXT;
import static com.sap.sailing.selenium.api.core.ApiContext.createAdminApiContext;
import static com.sap.sailing.selenium.api.core.ApiContext.createApiContext;
import static com.sap.sailing.selenium.pages.adminconsole.AdminConsolePage.goToPage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Optional;
import java.util.UUID;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.common.CompetitorRegistrationType;
import com.sap.sailing.selenium.api.core.ApiContext;
import com.sap.sailing.selenium.api.event.EventApi;
import com.sap.sailing.selenium.api.event.EventApi.Event;
import com.sap.sailing.selenium.api.event.LeaderboardApi;
import com.sap.sailing.selenium.api.event.SecurityApi;
import com.sap.sailing.selenium.api.event.UserGroupApi;
import com.sap.sailing.selenium.api.event.UserGroupApi.UserGroup;
import com.sap.sailing.selenium.api.regatta.Competitor;
import com.sap.sailing.selenium.api.regatta.RaceColumn;
import com.sap.sailing.selenium.api.regatta.Regatta;
import com.sap.sailing.selenium.api.regatta.RegattaApi;
import com.sap.sailing.selenium.api.regatta.RegattaRaces;
import com.sap.sailing.selenium.pages.adminconsole.AdminConsolePage;
import com.sap.sailing.selenium.test.AbstractSeleniumTest;
public class RegattaApiTest extends AbstractSeleniumTest {
private static String EVENT_NAME = "<ppp> loggingsession";
private static String OTHER_EVENT_NAME = "<xxx> loggingsession";
private static String BOAT_CLASS = "75QMNATIONALEKREUZER";
private final EventApi eventApi = new EventApi();
private final RegattaApi regattaApi = new RegattaApi();
private final LeaderboardApi leaderboardApi = new LeaderboardApi();
private final UserGroupApi usergroupApi = new UserGroupApi();
@Before
public void setUp() {
clearState(getContextRoot(), /* headless */ true);
super.setUp();
}
@Test
@@ -73,8 +88,8 @@ public class RegattaApiTest extends AbstractSeleniumTest {
RegattaRaces regattaRaces = regattaApi.getRegattaRaces(ctx, EVENT_NAME);
assertEquals("read: regatta is different", EVENT_NAME, regattaRaces.getRegattaName());
// assertEquals("read: reagtta.series should have 0 entries", 0, regattaRaces.getRaces().length);
//assertEquals("read: reagtta.series should have 0 entries", 0, regattaRaces.getRaces().length);
RaceColumn[] raceColumns = regattaApi.addRaceColumn(ctx, EVENT_NAME, "R", 1);
leaderboardApi.startRaceLogTracking(ctx, EVENT_NAME, raceColumns[0].getRaceName(), "Default");
regattaRaces = regattaApi.getRegattaRaces(ctx, EVENT_NAME);
@@ -136,4 +151,81 @@ public class RegattaApiTest extends AbstractSeleniumTest {
// assertEquals("read: racecolumn.seriesname is different", "Default", result[4].getSeriesName());
// assertEquals("read: racecolumn.racename is different", "T5", result[4].getRaceName());
}
@Test
public void testRegisterExistingCompetitorWithSecret() {
final AdminConsolePage adminConsole = goToPage(getWebDriver(), getContextRoot());
adminConsole.goToLocalServerPanel().setSelfServiceServer(true);
SecurityApi securityApi = new SecurityApi();
final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT);
securityApi.createUser(adminSecurityCtx, "donald", "Donald Duck", null, "daisy0815");
final ApiContext userWithCompetitorCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "donald", "daisy0815");
final ApiContext userWithCompetitorAndSecurityCtx = createApiContext(getContextRoot(),
ApiContext.SECURITY_CONTEXT, "donald", "daisy0815");
securityApi.createUser(adminSecurityCtx, "dagobert", "Dagobert Duck", null, "daisy1337");
final ApiContext userOwningEventCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "dagobert", "daisy1337");
// TODO we currently can't just create a competitor using the API. This is only possible by using a temporary Event/Regatta.
eventApi.createEvent(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, CLOSED, "default");
Competitor competitor = regattaApi.createAndAddCompetitor(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, null, "donald", "USA");
// ensure the reader can actually see the competitor
UserGroup group = usergroupApi.getUserGroupByName(userWithCompetitorAndSecurityCtx, "donald-tenant");
usergroupApi.addRoleToGroup(userWithCompetitorAndSecurityCtx, group.getGroupId(),
UUID.fromString(/* sailing viewer role id */"c42948df-517b-45cb-9fa9-d1e79f18e115"), true);
// This is the event to register the existing competitor for
Event eventToRegisterExistingCompetitor = eventApi.createEvent(userOwningEventCtx, OTHER_EVENT_NAME, BOAT_CLASS, CompetitorRegistrationType.OPEN_UNMODERATED, "default");
regattaApi.addCompetitor(userWithCompetitorCtx, OTHER_EVENT_NAME, competitor.getId(), Optional.of(eventToRegisterExistingCompetitor.getSecret()));
Competitor[] competitors = regattaApi.getCompetitors(userOwningEventCtx, OTHER_EVENT_NAME);
assertEquals(1, competitors.length);
assertEquals(competitor.getId(), competitors[0].getId());
}
@Test
public void testRegisterExistingCompetitorWithBadSecret() {
final AdminConsolePage adminConsole = goToPage(getWebDriver(), getContextRoot());
adminConsole.goToLocalServerPanel().setSelfServiceServer(true);
SecurityApi securityApi = new SecurityApi();
final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT);
securityApi.createUser(adminSecurityCtx, "donald", "Donald Duck", null, "daisy0815");
final ApiContext userWithCompetitorCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "donald",
"daisy0815");
securityApi.createUser(adminSecurityCtx, "dagobert", "Dagobert Duck", null, "daisy1337");
final ApiContext userOwningEventCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "dagobert",
"daisy1337");
final ApiContext userWithCompetitorAndSecurityCtx = createApiContext(getContextRoot(),
ApiContext.SECURITY_CONTEXT, "donald", "daisy0815");
// TODO we currently can't just create a competitor using the API. This is only possible by using a temporary
// Event/Regatta.
eventApi.createEvent(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, CLOSED, "default");
Competitor competitor = regattaApi.createAndAddCompetitor(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, null,
"donald", "USA");
// ensure the reader can actually see the competitor
UserGroup group = usergroupApi.getUserGroupByName(userWithCompetitorAndSecurityCtx, "donald-tenant");
usergroupApi.addRoleToGroup(userWithCompetitorAndSecurityCtx, group.getGroupId(),
UUID.fromString(/* sailing viewer role id */"c42948df-517b-45cb-9fa9-d1e79f18e115"), true);
// This is the event to register the existing competitor for
Event eventToRegisterExistingCompetitor = eventApi.createEvent(userOwningEventCtx, OTHER_EVENT_NAME, BOAT_CLASS,
CompetitorRegistrationType.OPEN_UNMODERATED, "default");
try {
regattaApi.addCompetitor(userWithCompetitorCtx, OTHER_EVENT_NAME, competitor.getId(),
Optional.of(eventToRegisterExistingCompetitor.getSecret() + "bad"));
Assert.fail("Expected error because of bad secret.");
} catch (RuntimeException e) {
Assert.assertTrue("Exepcted HTTP 401 - Unauthorized", e.getMessage().contains("rc=401"));
}
Competitor[] competitors = regattaApi.getCompetitors(userOwningEventCtx, OTHER_EVENT_NAME);
assertEquals(0, competitors.length);
}
}
@@ -0,0 +1,332 @@
package com.sap.sailing.selenium.api.test;
import static com.sap.sailing.selenium.api.core.ApiContext.SERVER_CONTEXT;
import static com.sap.sailing.selenium.api.core.ApiContext.createAdminApiContext;
import static com.sap.sailing.selenium.api.core.ApiContext.createApiContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.common.CompetitorRegistrationType;
import com.sap.sailing.selenium.api.core.ApiContext;
import com.sap.sailing.selenium.api.event.EventApi;
import com.sap.sailing.selenium.api.event.EventApi.Event;
import com.sap.sailing.selenium.api.event.SecurityApi;
import com.sap.sailing.selenium.api.event.TrackedEventsApi;
import com.sap.sailing.selenium.api.event.TrackedEventsApi.TrackedElement;
import com.sap.sailing.selenium.api.event.TrackedEventsApi.TrackedEvent;
import com.sap.sailing.selenium.api.event.TrackedEventsApi.TrackedEvents;
import com.sap.sailing.selenium.test.AbstractSeleniumTest;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Triple;
public class TrackedEventsResourceApiTest extends AbstractSeleniumTest {
private final TrackedEventsApi trackedEventsApi = new TrackedEventsApi();
private final EventApi eventApi = new EventApi();
private final SecurityApi securityApi = new SecurityApi();
@Before
public void setUp() {
clearState(getContextRoot());
}
@Test
public void testGetTrackedEventsEmpty() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true);
Assert.assertNotNull("Tracked Events element should not be null", trackedEvents);
Assert.assertTrue("Expected empty list of tracked events", Util.isEmpty(trackedEvents.getEvents()));
}
@Test
public void testCreateMultipleTrackingEventsAndEditOne() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
final String competitorId = UUID.randomUUID().toString();
final String boatId = UUID.randomUUID().toString();
final String markId = UUID.randomUUID().toString();
final String eventBaseUrl = "testUrl";
final String deviceId = UUID.randomUUID().toString();
final Event evt = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(),
"75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim");
final String eventId = evt.getId();
final String leaderboardName = evt.getName();
final String regattaSecret = evt.getSecret();
final Event evt2 = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(),
"75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim");
final String eventId2 = evt2.getId();
final String leaderboardName2 = evt2.getName();
final String regattaSecret2 = evt.getSecret();
final Set<Triple<String, String, String>> trackedIds = new HashSet<>();
trackedIds.add(new Triple<>(competitorId, null, null));
trackedIds.add(new Triple<>(null, boatId, null));
trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId,
trackedIds, regattaSecret);
trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId2, leaderboardName2, eventBaseUrl, deviceId,
new HashSet<>(Arrays.asList(new Triple<>(null, null, markId))), regattaSecret2);
trackedEventsApi.setArchived(adminCtx, evt.getId(), leaderboardName, true);
trackedEventsApi.setArchived(adminCtx, evt2.getId(), leaderboardName2, false);
// check if created event is still there
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, false);
int cntEvents = 0;
for (final TrackedEvent event : trackedEvents.getEvents()) {
cntEvents++;
Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl());
if (eventId.equals(event.getEventId())) {
Assert.assertEquals("Unexpected event ID", eventId, event.getEventId());
Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName());
Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret());
Assert.fail("Expected event 1 to not be shown anymore.");
} else if (eventId2.equals(event.getEventId())) {
Assert.assertEquals("Unexpected event ID", eventId2, event.getEventId());
Assert.assertEquals("Unexpected leaderboard name", leaderboardName2, event.getLeaderboardName());
Assert.assertEquals("Unexpected regatta secret", regattaSecret2, event.getRegattaSecret());
} else {
Assert.fail("Invalid event id.");
}
int cntElements = 0;
for (final TrackedElement elem : event.getTrackedElements()) {
cntElements++;
final boolean correctBoatId = boatId.equals(elem.getBoatId());
final boolean correctCompetitorId = competitorId.equals(elem.getCompetitorId());
final boolean correctMarkId = markId.equals(elem.getMarkId());
Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId());
Assert.assertTrue("More than one or zero items tracked in this element",
correctBoatId ^ correctCompetitorId ^ correctMarkId);
}
Assert.assertEquals("Invalid numer of elements in this event", 1, cntElements);
}
Assert.assertEquals("Expected 1 events", 1, cntEvents);
}
@Test
public void testCreateMultipleTrackingEvents() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
final String competitorId = UUID.randomUUID().toString();
final String boatId = UUID.randomUUID().toString();
final String markId = UUID.randomUUID().toString();
final String eventBaseUrl = "testUrl";
final String deviceId = UUID.randomUUID().toString();
final Event evt = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(),
"75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim");
final String eventId = evt.getId();
final String leaderboardName = evt.getName();
final String regattaSecret = evt.getSecret();
final Event evt2 = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(),
"75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim");
final String eventId2 = evt2.getId();
final String leaderboardName2 = evt2.getName();
final String regattaSecret2 = evt2.getSecret();
final Set<Triple<String, String, String>> trackedIds = new HashSet<>();
trackedIds.add(new Triple<>(competitorId, null, null));
trackedIds.add(new Triple<>(null, boatId, null));
trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId,
trackedIds, regattaSecret);
trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId2, leaderboardName2, eventBaseUrl, deviceId,
new HashSet<>(Arrays.asList(new Triple<>(null, null, markId))), regattaSecret2);
// check if created event is still there
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true);
int cntEvents = 0;
for (final TrackedEvent event : trackedEvents.getEvents()) {
cntEvents++;
Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl());
if (eventId.equals(event.getEventId())) {
Assert.assertEquals("Unexpected event ID", eventId, event.getEventId());
Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName());
Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret());
} else if (eventId2.equals(event.getEventId())) {
Assert.assertEquals("Unexpected event ID", eventId2, event.getEventId());
Assert.assertEquals("Unexpected leaderboard name", leaderboardName2, event.getLeaderboardName());
Assert.assertEquals("Unexpected regatta secret", regattaSecret2, event.getRegattaSecret());
} else {
Assert.fail("Invalid event id.");
}
int cntElements = 0;
for (final TrackedElement elem : event.getTrackedElements()) {
cntElements++;
final boolean correctBoatId = boatId.equals(elem.getBoatId());
final boolean correctCompetitorId = competitorId.equals(elem.getCompetitorId());
final boolean correctMarkId = markId.equals(elem.getMarkId());
Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId());
Assert.assertTrue("More than one or zero items tracked in this element",
correctBoatId ^ correctCompetitorId ^ correctMarkId);
}
Assert.assertEquals("Invalid numer of elements in this event", eventId.equals(event.getEventId()) ? 2 : 1,
cntElements);
}
Assert.assertEquals("Expected 2 events", 2, cntEvents);
}
@Test
public void testCreateMultipleTrackingsForSameEvent() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
final String eventName = "TestEvent-" + UUID.randomUUID().toString();
final String competitorId = UUID.randomUUID().toString();
final String boatId = UUID.randomUUID().toString();
final String markId = UUID.randomUUID().toString();
final String eventBaseUrl = "testUrl";
final String deviceId = UUID.randomUUID().toString();
final Event evt = eventApi.createEvent(adminCtx, eventName, "75QMNATIONALEKREUZER",
CompetitorRegistrationType.CLOSED, "Mannheim");
final String eventId = evt.getId();
final String leaderboardName = evt.getName();
final String regattaSecret = evt.getSecret();
final Set<Triple<String, String, String>> trackedIds = new HashSet<>();
trackedIds.add(new Triple<>(competitorId, null, null));
trackedIds.add(new Triple<>(null, boatId, null));
trackedIds.add(new Triple<>(null, null, markId));
trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId,
trackedIds, regattaSecret);
// check if created event is still there
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true);
int cntEvents = 0;
for (final TrackedEvent event : trackedEvents.getEvents()) {
cntEvents++;
Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl());
Assert.assertEquals("Unexpected event ID", eventId, event.getEventId());
Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName());
Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret());
int cntElements = 0;
for (final TrackedElement elem : event.getTrackedElements()) {
cntElements++;
final boolean correctBoatId = boatId.equals(elem.getBoatId());
final boolean correctCompetitorId = competitorId.equals(elem.getCompetitorId());
final boolean correctMarkId = markId.equals(elem.getMarkId());
Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId());
Assert.assertTrue("More than one or zero items tracked in this element",
correctBoatId ^ correctCompetitorId ^ correctMarkId);
}
Assert.assertEquals("Expected 3 event elements", 3, cntElements);
}
Assert.assertEquals("Expected exactly 1 event", 1, cntEvents);
}
@Test
public void testCreateTrackedEventWithCompetitorTracking() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
testCreateTrackedEventWithTracking(UUID.randomUUID().toString(), null, null, adminCtx,
"TestEvent-" + UUID.randomUUID().toString());
}
@Test
public void testCreateTrackedEventWithBoatTracking() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
testCreateTrackedEventWithTracking(null, UUID.randomUUID().toString(), null, adminCtx,
"TestEvent-" + UUID.randomUUID().toString());
}
@Test
public void testCreateTrackedEventWithMarkTracking() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
testCreateTrackedEventWithTracking(null, null, UUID.randomUUID().toString(), adminCtx,
"TestEvent-" + UUID.randomUUID().toString());
}
private String testCreateTrackedEventWithTracking(final String competitorId, final String boatId,
final String markId, final ApiContext adminCtx, final String eventName) {
final String eventBaseUrl = "testUrl";
final String deviceId = UUID.randomUUID().toString();
final Event evt = eventApi.createEvent(adminCtx, eventName, "75QMNATIONALEKREUZER",
CompetitorRegistrationType.CLOSED, "Mannheim");
final String eventId = evt.getId();
final String leaderboardName = evt.getName();
final String regattaSecret = evt.getSecret();
trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId,
new HashSet<>(Arrays.asList(new Triple<>(competitorId, boatId, markId))), regattaSecret);
// check if created event is still there
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true);
boolean hasEvents = false;
for (final TrackedEvent event : trackedEvents.getEvents()) {
hasEvents = true;
Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl());
Assert.assertEquals("Unexpected event ID", eventId, event.getEventId());
Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName());
Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret());
boolean hasElements = false;
for (final TrackedElement elem : event.getTrackedElements()) {
hasElements = true;
Assert.assertEquals("Unexpected boat ID", boatId, elem.getBoatId());
Assert.assertEquals("Unexpected competitor ID", competitorId, elem.getCompetitorId());
Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId());
Assert.assertEquals("Unexpected mark ID", markId, elem.getMarkId());
}
Assert.assertTrue("Expected at least one element", hasElements);
}
Assert.assertTrue("Expected at least one event", hasEvents);
return eventId;
}
@Test
public void testDeleteEvent() {
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
final String leaderboardName = "TestEvent-" + UUID.randomUUID().toString();
final String eventId = testCreateTrackedEventWithTracking(UUID.randomUUID().toString(), null, null, adminCtx,
leaderboardName);
trackedEventsApi.deleteEventTrackings(adminCtx, eventId, leaderboardName);
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true);
Assert.assertNotNull("Tracked Events element should not be null", trackedEvents);
Assert.assertTrue("Expected empty list of tracked events", Util.isEmpty(trackedEvents.getEvents()));
}
@Test
public void testMultipleUsers() {
// create test user
final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT);
securityApi.createUser(adminSecurityCtx, "tuser", "Test User", null, "testuser");
final ApiContext userCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "tuser", "testuser");
final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
// add an event in the user context of admin user
testCreateTrackedEventWithTracking(null, UUID.randomUUID().toString(), null, adminCtx,
"TestEvent-" + UUID.randomUUID().toString());
// check if event is invisible to the other user
final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(userCtx, true);
Assert.assertNotNull("Tracked Events element should not be null", trackedEvents);
Assert.assertTrue("Expected empty list of tracked events", Util.isEmpty(trackedEvents.getEvents()));
}
}
@@ -349,13 +349,17 @@ public class RegattasResource extends AbstractSailingServerResource {
@Produces("application/json;charset=UTF-8")
@Path("{regattaname}/competitors/{competitorid}/add")
public Response addCompetitor(@PathParam("regattaname") String regattaName,
@PathParam("competitorid") String competitorIdAsString) {
@PathParam("competitorid") String competitorIdAsString,
@QueryParam("secret") String registrationLinkSecret) {
Response response;
Regatta regatta = findRegattaByName(regattaName);
if (regatta == null) {
response = getBadRegattaErrorResponse(regattaName);
} else {
getSecurityService().checkCurrentUserUpdatePermission(regatta);
boolean skipPermissionCheck = getService().skipChecksDueToCorrectSecret(regattaName, registrationLinkSecret);
if (!skipPermissionCheck) {
getSecurityService().checkCurrentUserUpdatePermission(regatta);
}
Serializable competitorId;
try {
competitorId = UUID.fromString(competitorIdAsString);
@@ -35,6 +35,7 @@ public class RestApiApplication extends Application {
classes.add(TagsResource.class);
classes.add(UserGroupsResource.class);
classes.add(StatusResource.class);
classes.add(TrackedEventsResource.class);
// Exception Mappers
classes.add(ShiroAuthorizationExceptionTo401ResponseMapper.class);
@@ -0,0 +1,314 @@
package com.sap.sailing.server.gateway.jaxrs.api;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException;
import com.sap.sailing.server.gateway.deserialization.impl.Helpers;
import com.sap.sailing.server.gateway.jaxrs.AbstractSailingServerResource;
import com.sap.sailing.server.impl.preferences.model.SailingPreferences;
import com.sap.sailing.server.impl.preferences.model.TrackedElementWithDeviceId;
import com.sap.sailing.server.impl.preferences.model.TrackedEventPreference;
import com.sap.sailing.server.impl.preferences.model.TrackedEventPreferences;
import com.sap.sse.security.shared.impl.User;
@Path("/v1/trackedevents/")
public class TrackedEventsResource extends AbstractSailingServerResource {
private static final String KEY_QUERY_INCLUDE_ARCHIVED = "includeArchived";
private static final String KEY_LEADERBOARD_NAME = "leaderboardName";
private static final String KEY_EVENT_ID = "eventId";
private static final String KEY_EVENT_IS_ARCHIVED = "isArchived";
private static final String KEY_EVENT_TRACKED_ELEMENTS = "trackedElements";
private static final String KEY_EVENT_BASE_URL = "url";
private static final String KEY_EVENT_REGATTA_SECRET = "regattaSecret";
private static final String KEY_TRACKED_ELEMENT_DEVICE_ID = "deviceId";
private static final String KEY_TRACKED_ELEMENT_COMPETITOR_ID = "competitorId";
private static final String KEY_TRACKED_ELEMENT_BOAT_ID = "boatId";
private static final String KEY_TRACKED_ELEMENT_MARK_ID = "markId";
private static final String KEY_TRACKED_EVENTS = "trackedEvents";
@GET
@Produces("application/json;charset=UTF-8")
public Response getTrackedEvents(@QueryParam(KEY_QUERY_INCLUDE_ARCHIVED) String includeArchivedStr) {
final User currentUser = getSecurityService().getCurrentUser();
final ResponseBuilder builder;
// check if user logged in
if (currentUser != null) {
// load tracked events from storage
final TrackedEventPreferences prefs = getSecurityService().getPreferenceObject(currentUser.getName(),
SailingPreferences.TRACKED_EVENTS_PREFERENCES);
final JSONArray result = new JSONArray();
final boolean includeArchived = Boolean.parseBoolean(includeArchivedStr);
if (prefs != null) {
// iterate all stored tracked events
for (final TrackedEventPreference pref : prefs.getTrackedEvents()) {
if (!includeArchived && pref.getIsArchived()) {
// skip, if event is archived and should be filtered out
continue;
}
final JSONObject jsonEvent = new JSONObject();
jsonEvent.put(KEY_EVENT_ID, pref.getEventId().toString());
jsonEvent.put(KEY_EVENT_REGATTA_SECRET, pref.getRegattaSecret());
jsonEvent.put(KEY_EVENT_BASE_URL, pref.getBaseUrl());
jsonEvent.put(KEY_EVENT_IS_ARCHIVED, pref.getIsArchived());
jsonEvent.put(KEY_LEADERBOARD_NAME, pref.getLeaderboardName());
jsonEvent.put(KEY_EVENT_TRACKED_ELEMENTS, trackedElementsToJson(pref));
result.add(jsonEvent);
}
}
// storage was empty -> respond with empty list
final JSONObject resultEvents = new JSONObject();
resultEvents.put(KEY_TRACKED_EVENTS, result);
final String jsonString = resultEvents.toJSONString();
builder = Response.ok(jsonString).header("Content-Type", MediaType.APPLICATION_JSON + ";charset=UTF-8");
} else {
builder = Response.status(Status.UNAUTHORIZED);
}
return builder.build();
}
private JSONArray trackedElementsToJson(final TrackedEventPreference pref) {
final JSONArray deviceIdsWithTrackedElementJson = new JSONArray();
for (final TrackedElementWithDeviceId trackedElement : pref.getTrackedElements()) {
final JSONObject trackedElementJson = new JSONObject();
trackedElementJson.put(KEY_TRACKED_ELEMENT_DEVICE_ID, trackedElement.getDeviceId());
if (trackedElement.getTrackedCompetitorId() != null) {
trackedElementJson.put(KEY_TRACKED_ELEMENT_COMPETITOR_ID,
trackedElement.getTrackedCompetitorId().toString());
} else if (trackedElement.getTrackedBoatId() != null) {
trackedElementJson.put(KEY_TRACKED_ELEMENT_BOAT_ID, trackedElement.getTrackedBoatId().toString());
} else if (trackedElement.getTrackedMarkId() != null) {
trackedElementJson.put(KEY_TRACKED_ELEMENT_MARK_ID, trackedElement.getTrackedMarkId().toString());
}
deviceIdsWithTrackedElementJson.add(trackedElementJson);
}
return deviceIdsWithTrackedElementJson;
}
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("{eventId}/{leaderboardName}")
public Response updateOrCreateTrackedEvent(@PathParam(KEY_EVENT_ID) String eventId,
@PathParam(KEY_LEADERBOARD_NAME) String leaderboardName, String jsonBody) {
ResponseBuilder responseBuilder = null;
final User currentUser = getSecurityService().getCurrentUser();
if (currentUser != null) {
try {
final Object requestBody = JSONValue.parseWithException(jsonBody);
final JSONObject requestObject = Helpers.toJSONObjectSafe(requestBody);
final Boolean archived = (Boolean) requestObject.get(KEY_EVENT_IS_ARCHIVED);
final String baseUrl = (String) requestObject.get(KEY_EVENT_BASE_URL);
final String regattaSecret = (String) requestObject.get(KEY_EVENT_REGATTA_SECRET);
try {
final UUID uuidEvent = UUID.fromString(eventId);
final boolean isArchived = archived;
TrackedEventPreferences prefs = getSecurityService().getPreferenceObject(currentUser.getName(),
SailingPreferences.TRACKED_EVENTS_PREFERENCES);
if (prefs == null) {
prefs = new TrackedEventPreferences();
}
final JSONArray trackedElementsJson = (JSONArray) requestObject.get(KEY_EVENT_TRACKED_ELEMENTS);
// check that exactly one trackedElement is in JSON
if (trackedElementsJson == null || trackedElementsJson.size() == 0) {
// too few children
responseBuilder = Response.status(Status.BAD_REQUEST)
.entity("Invalid JSON body in request: Tracked element is missing.");
} else {
// Copy tracked event preference objects to new list while removing the changed
// event/leaderboard node
final Collection<TrackedEventPreference> prefsNew = //
StreamSupport.stream(prefs.getTrackedEvents().spliterator(), false)
.filter(pref -> !(pref.getEventId().equals(uuidEvent)
&& pref.getLeaderboardName().equals(leaderboardName)))
.collect(Collectors.toList());
// Create new tracked elements for this event/leaderboard from JSON body
final Collection<TrackedElementWithDeviceId> trackedElements = new ArrayList<>();
for (int i = 0; i < trackedElementsJson.size(); i++) {
final JSONObject jsonTrackedElement = (JSONObject) trackedElementsJson.get(i);
// parse IDs
final String deviceId = (String) jsonTrackedElement.get(KEY_TRACKED_ELEMENT_DEVICE_ID);
final String competitorIdStr = (String) jsonTrackedElement
.get(KEY_TRACKED_ELEMENT_COMPETITOR_ID);
final String boatIdStr = (String) jsonTrackedElement.get(KEY_TRACKED_ELEMENT_BOAT_ID);
final String markIdStr = (String) jsonTrackedElement.get(KEY_TRACKED_ELEMENT_MARK_ID);
// parse UUIDs of tracked element
final UUID competitorId = parseUUID(competitorIdStr);
final UUID boatId = parseUUID(boatIdStr);
final UUID markId = parseUUID(markIdStr);
if (boatId != null ^ markId != null ^ competitorId != null) {
// create TrackedElementWithID holder
final TrackedElementWithDeviceId newPrefElem = new TrackedElementWithDeviceId(deviceId,
boatId, competitorId, markId);
trackedElements.add(newPrefElem);
} else {
// no boatId, competitorId or markId were specified
responseBuilder = Response.status(Status.BAD_REQUEST)
.entity("Invalid JSON body in request.");
}
}
final TrackedEventPreference newPreference = new TrackedEventPreference(uuidEvent,
leaderboardName, trackedElements, baseUrl, isArchived, regattaSecret);
prefsNew.add(newPreference);
prefs.setTrackedEvents(prefsNew);
getSecurityService().setPreferenceObject(currentUser.getName(),
SailingPreferences.TRACKED_EVENTS_PREFERENCES, prefs);
responseBuilder = Response.status(Status.ACCEPTED);
}
} catch (IllegalArgumentException | ClassCastException e) {
responseBuilder = Response.status(Status.BAD_REQUEST)
.entity("Invalid or missing attributes in JSON body.");
}
} catch (ParseException | JsonDeserializationException e) {
responseBuilder = Response.status(Status.BAD_REQUEST).entity("Invalid JSON body in request.");
}
} else {
responseBuilder = Response.status(Status.UNAUTHORIZED);
}
return responseBuilder.build();
}
private UUID parseUUID(String potentialUUID) {
UUID uuid;
if (potentialUUID != null && !potentialUUID.isEmpty()) {
try {
uuid = UUID.fromString(potentialUUID);
} catch (IllegalArgumentException e) {
uuid = null;
}
} else {
uuid = null;
}
return uuid;
}
@POST
@Path("{eventId}/{leaderboardName}")
public Response updateTrackedEventsArchivedStatus(@PathParam(KEY_EVENT_ID) String eventId,
@PathParam(KEY_LEADERBOARD_NAME) String leaderboardName,
@FormParam(KEY_QUERY_INCLUDE_ARCHIVED) String archived) {
final boolean isArchived = Boolean.parseBoolean(archived);
return applyOnEventByIdAndLeadboardName(eventId, leaderboardName,
pref -> new TrackedEventPreference(pref, isArchived));
}
@DELETE
@Path("{eventId}/{leaderboardName}")
public Response deleteTrackedEvents(@PathParam(KEY_EVENT_ID) String eventId,
@PathParam(KEY_LEADERBOARD_NAME) String leaderboardName) {
return applyOnEventByIdAndLeadboardName(eventId, leaderboardName, pref -> null);
}
/** Finds the tracked event with the corresponding eventId and applies the function to it. */
private Response applyOnEventByIdAndLeadboardName(String eventId, String leaderboardName,
Function<TrackedEventPreference, TrackedEventPreference> function) {
ResponseBuilder responseBuilder;
final User currentUser = getSecurityService().getCurrentUser();
if (currentUser != null) {
final TrackedEventPreferences prefs = getSecurityService().getPreferenceObject(currentUser.getName(),
SailingPreferences.TRACKED_EVENTS_PREFERENCES);
try {
// parse tracked event UUID
final UUID eventUuid = UUID.fromString(eventId);
if (prefs == null) {
// storage is empty
responseBuilder = Response.status(Status.NOT_FOUND)
.entity("No tracked events with this eventId found.");
} else {
// iterate stored preferences until tracked event with corresponding eventId is found
boolean found = false;
final Collection<TrackedEventPreference> newPrefs = new HashSet<>();
for (final TrackedEventPreference pref : prefs.getTrackedEvents()) {
if (pref.getEventId().equals(eventUuid) && pref.getLeaderboardName().equals(leaderboardName)) {
// tracked event found, apply function on it and remove it if result of function execution
// is null
found = true;
final TrackedEventPreference updatedPreference = function.apply(pref);
if (updatedPreference != null) {
newPrefs.add(updatedPreference);
}
} else {
newPrefs.add(pref);
}
}
if (!found) {
// tracked event was not found -> respond with error
responseBuilder = Response.status(Status.NOT_FOUND)
.entity("No tracked events with this eventId found.");
} else {
// update and store preferences
prefs.setTrackedEvents(newPrefs);
getSecurityService().setPreferenceObject(currentUser.getName(),
SailingPreferences.TRACKED_EVENTS_PREFERENCES, prefs);
responseBuilder = Response.status(Status.ACCEPTED);
}
}
} catch (IllegalArgumentException e) {
// eventId parameter could not be parsed
responseBuilder = Response.status(Status.BAD_REQUEST).entity("Invalid eventId.");
}
} else {
responseBuilder = Response.status(Status.UNAUTHORIZED);
}
return responseBuilder.build();
}
}
@@ -154,7 +154,7 @@ name to make sure you get to the event you're interested in.
<td>Creates a competitor and adds it to the regatta</td>
</tr>
<tr>
<td>/api/v1/regattas/{regattaname}/competitors/{competitorId}/add</td>
<td><a href="regattasCompetitorAdd.html">/api/v1/regattas/{regattaname}/competitors/{competitorId}/add</a></td>
<td>Registers a competitor with a regatta (POST)</td>
</tr>
<tr>
@@ -340,6 +340,22 @@ name to make sure you get to the event you're interested in.
<td><a href="status.html">/api/v1/status</a></td>
<td>Returns a status description for the "RacingEventService" core service</td>
</tr>
<tr>
<td><a href="trackedeventsGetDoc.html">/api/v1/trackedevents</a></td>
<td>Gets all tracked events with the associated device-to-competitor/boat/mark elements for the current user.</td>
</tr>
<tr>
<td><a href="trackedeventsPutDoc.html">/api/v1/trackedevents</a></td>
<td>Create or update a tracked event for the current user.</td>
</tr>
<tr>
<td><a href="trackedeventsPostDoc.html">/api/v1/trackedevents</a></td>
<td>Update the archived status of a tracked event for the current user.</td>
</tr>
<tr>
<td><a href="trackedeventsDeleteDoc.html">/api/v1/trackedevents</a></td>
<td>Delete a tracked event for the current user.</td>
</tr>
</tbody>
</table>
<div style="height: 1em;"></div>
@@ -0,0 +1,50 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/regattas/{regattaname}/competitors/{competitorId}/add</h2>
<b>Description:</b>
<p>Registers an existing competitor with the regatta.<br/>
This provides functionality <li> for the regatta owner (or any user with update permission on the regatta) to add existing
competitors to the regatta <li> and for any user to register himself to an regatta that is explicitly open for self-registrations.<br/>
All slashes contained within the {regattaname} parameter must be replaced by "__" (double underscore).</p>
<br/>
<table>
<tr>
<td>Webservice Type:</td>
<td>POST</td>
</tr>
<tr>
<td>Output format:</td>
<td>None</td>
</tr>
<tr>
<td>Mandatory parameters:</td>
<td>
<div>regattaname the name of the regatta to register a competitor for</div>
<div>competitorid the UUID of the competitor to register</div>
</td>
</tr>
<tr>
<td>Additional mandatory parameters for user self-registrations on open regattas:</td>
<td>
<div>secret secret string required for self registration validation purpose</div>
</td>
</tr>
<tr>
<td>Request method:</td>
<td>POST</td>
</tr>
</table>
<div style="height: 1em;"></div>
<a href="index.html">Back to Web Service Overview</a>
</body>
</html>
@@ -0,0 +1,52 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen'
rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet'
title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/trackedevents/{eventId}/{leaderboardName}</h2>
<b>Description:</b>
<p>Delete a tracked event for the current user.</p>
<br />
<table>
<tr>
<td>Webservice Type:</td>
<td>REST</td>
</tr>
<tr>
<td>Output format:</td>
<td>Json</td>
</tr>
<tr>
<td>Mandatory parameters:</td>
<td>Path parameter eventId: Identifier of the associated event<br />
Path parameter leaderboardName: Name of the associated leaderboard
</td>
</tr>
<tr>
<td>Optional parameters:</td>
<td>None</td>
</tr>
<tr>
<td>Request method:</td>
<td>DELETE</td>
</tr>
<tr>
<td>Example:</td>
<td>Request:
http://www.sapsailing.com/sailingserver/api/v1/trackedevents?eventId=71fe232b-19b8-455f-ac91-23a53a5c557a&leaderbaordName=TestRegattaLeaderboard<br />
Status 202 <br />
</tr>
</table>
<div style="height: 1em;"></div>
<a href="index.html">Back to Web Service Overview</a>
</body>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen'
rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet'
title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/trackedevents</h2>
<b>Description:</b>
<p>Gets all tracked events with the associated
device-to-competitor/boat/mark elements for the current user.</p>
<br />
<table>
<tr>
<td>Webservice Type:</td>
<td>REST</td>
</tr>
<tr>
<td>Output format:</td>
<td>Json</td>
</tr>
<tr>
<td>Mandatory parameters:</td>
<td>includeArchived: Should be set to true, if the archived
elements should be returned as well. Default value false returns
only non-archived elements.</td>
</tr>
<tr>
<td>Optional parameters:</td>
<td>None</td>
</tr>
<tr>
<td>Request method:</td>
<td>GET</td>
</tr>
<tr>
<td>Example:</td>
<td><a
href="http://www.sapsailing.com/sailingserver/api/v1/trackedevents?includeArchived=false">http://www.sapsailing.com/sailingserver/api/v1/trackedevents?includeArchived=false</a><br />
Status 200</td>
</tr>
</table>
<div style="height: 1em;"></div>
<a href="index.html">Back to Web Service Overview</a>
</body>
</html>
@@ -0,0 +1,55 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen'
rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet'
title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/trackedevents</h2>
<b>Description:</b>
<p>Update the archived status of a tracked event for the current
user.</p>
<br />
<table>
<tr>
<td>Webservice Type:</td>
<td>REST</td>
</tr>
<tr>
<td>Output format:</td>
<td>Json</td>
</tr>
<tr>
<td>Mandatory parameters:</td>
<td>eventId (Path parameter): Identifier of the associated event<br />
leaderboardName (Path parameter): Name of the associated leaderboard<br />
isArchived (Form parameter): True or False depending on whether the
event should be set to archived or non-archived.
</td>
</tr>
<tr>
<td>Optional parameters:</td>
<td>None</td>
</tr>
<tr>
<td>Request method:</td>
<td>POST</td>
</tr>
<tr>
<td>Example:</td>
<td>Request:
http://www.sapsailing.com/sailingserver/api/v1/trackedevents/71fe232b-19b8-455f-ac91-23a53a5c557a/TestRegattaLeaderboard<br />
form: isArchived=true<br /> Status 202 <br />
</tr>
</table>
<div style="height: 1em;"></div>
<a href="index.html">Back to Web Service Overview</a>
</body>
</html>
@@ -0,0 +1,58 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen'
rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet'
title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/trackedevents/{eventId}/{leaderboardName}</h2>
<b>Description:</b>
<p>Create or update a tracked event for the current user and set (overwrite) all tracked elements.</p>
<br />
<table>
<tr>
<td>Webservice Type:</td>
<td>REST</td>
</tr>
<tr>
<td>Output format:</td>
<td>Json</td>
</tr>
<tr>
<td>Mandatory parameters:</td>
<td>Path Parameter eventId: Identifier of the associated event<br />
Path Parameter leaderboardName: Name of the associated leaderboard
</td>
</tr>
<tr>
<td>Optional parameters:</td>
<td>None</td>
</tr>
<tr>
<td>Request method:</td>
<td>PUT</td>
</tr>
<tr>
<td>Example:</td>
<td>Request:
http://www.sapsailing.com/sailingserver/api/v1/trackedevents/61fe232b-19b8-455f-ac91-23a53a5c557a/TestRegattaLeaderboard<br />
<br /> Body (device-to-competitor):<br />
{"url":"http://www.sapsailing.com","regattaSecret":"71fe232b-19b8-455f-ac91-23a53a5c557a","trackedElements":[{"deviceId":"301eda00-68ad-402d-9734-4a7d5b8df406","competitorId":"6204e6d2-2d6c-4891-b077-d83117c69513"}],"isArchived":false}<br />
<br /> Body(device-to-boat):<br />
{"url":"http://www.sapsailing.com","regattaSecret":"71fe232b-19b8-455f-ac91-23a53a5c557a","trackedElements":[{"deviceId":"301eda00-68ad-402d-9734-4a7d5b8df406","boatId":"6204e6d2-2d6c-4891-b077-d83117c69513"}],"isArchived":false}<br />
<br /> Body (device-to-mark):<br />
{"url":"http://www.sapsailing.com","regattaSecret":"71fe232b-19b8-455f-ac91-23a53a5c557a","trackedElements":[{"deviceId":"301eda00-68ad-402d-9734-4a7d5b8df406","markId":"6204e6d2-2d6c-4891-b077-d83117c69513"}],"isArchived":false}<br />
Status 202 <br />
</tr>
</table>
<div style="height: 1em;"></div>
<a href="index.html">Back to Web Service Overview</a>
</body>
</html>
@@ -46,6 +46,7 @@ import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPrefer
import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences;
import com.sap.sailing.server.impl.preferences.model.SailorProfilePreferences;
import com.sap.sailing.server.impl.preferences.model.StoredDataMiningQueryPreferences;
import com.sap.sailing.server.impl.preferences.model.TrackedEventPreferences;
import com.sap.sailing.server.interfaces.RacingEventService;
import com.sap.sailing.server.notification.impl.SailingNotificationServiceImpl;
import com.sap.sailing.server.security.SailingViewerRole;
@@ -153,12 +154,15 @@ public class Activator implements BundleActivator {
properties));
properties.put(PreferenceConverter.KEY_PARAMETER_NAME, StoredDataMiningQueryPreferences.PREF_NAME);
registrations.add(context.registerService(PreferenceConverter.class,
new GenericJSONPreferenceConverter<>(() -> new StoredDataMiningQueryPreferences()), properties));
new GenericJSONPreferenceConverter<>(StoredDataMiningQueryPreferences::new), properties));
properties.put(PreferenceConverter.KEY_PARAMETER_NAME, SailorProfilePreferences.PREF_NAME);
registrations.add(context.registerService(PreferenceConverter.class,
new GenericJSONPreferenceConverter<>(
() -> new SailorProfilePreferences(racingEventService.getCompetitorAndBoatStore())),
properties));
properties.put(PreferenceConverter.KEY_PARAMETER_NAME, TrackedEventPreferences.PREF_NAME);
registrations.add(context.registerService(PreferenceConverter.class,
new GenericJSONPreferenceConverter<>(TrackedEventPreferences::new), properties));
}
public static BundleContext getContext() {
@@ -7,4 +7,5 @@ package com.sap.sailing.server.impl.preferences.model;
public interface SailingPreferences {
public static final String NOTIFICATION_PREFERENCES_PREFIX = "sailing.notifications.";
public static final String STORED_DATAMINING_QUERY_PREFERENCES = "sailing.datamining.storedqueries";
public static final String TRACKED_EVENTS_PREFERENCES = "sailing.profile.trackedevents";
}
@@ -0,0 +1,62 @@
package com.sap.sailing.server.impl.preferences.model;
import java.util.UUID;
import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings;
import com.sap.sse.common.settings.generic.StringSetting;
import com.sap.sse.common.settings.generic.UUIDSetting;
public class TrackedElementWithDeviceId extends AbstractGenericSerializableSettings {
private static final long serialVersionUID = 234711768869820003L;
private transient StringSetting deviceId;
private transient UUIDSetting trackedCompetitorId;
private transient UUIDSetting trackedBoatId;
private transient UUIDSetting trackedMarkId;
public TrackedElementWithDeviceId() {
}
/** copy constructor */
public TrackedElementWithDeviceId(TrackedElementWithDeviceId other) {
this();
this.deviceId.setValue(other.getDeviceId());
this.trackedBoatId.setValue(other.getTrackedBoatId());
this.trackedCompetitorId.setValue(other.getTrackedCompetitorId());
this.trackedMarkId.setValue(other.getTrackedMarkId());
}
public TrackedElementWithDeviceId(String deviceId, UUID trackedBoatId, UUID trackedCompetitorId,
UUID trackedMarkId) {
this();
this.deviceId.setValue(deviceId);
this.trackedBoatId.setValue(trackedBoatId);
this.trackedCompetitorId.setValue(trackedCompetitorId);
this.trackedMarkId.setValue(trackedMarkId);
}
@Override
protected void addChildSettings() {
deviceId = new StringSetting("deviceId", this);
trackedCompetitorId = new UUIDSetting("trackedCompetitorId", this);
trackedBoatId = new UUIDSetting("trackedBoatId", this);
trackedMarkId = new UUIDSetting("trackedMarkId", this);
}
public String getDeviceId() {
return deviceId.getValue();
}
public UUID getTrackedBoatId() {
return trackedBoatId.getValue();
}
public UUID getTrackedCompetitorId() {
return trackedCompetitorId.getValue();
}
public UUID getTrackedMarkId() {
return trackedMarkId.getValue();
}
}
@@ -0,0 +1,110 @@
package com.sap.sailing.server.impl.preferences.model;
import java.util.Collection;
import java.util.HashSet;
import java.util.UUID;
import com.sap.sse.common.Util;
import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings;
import com.sap.sse.common.settings.generic.BooleanSetting;
import com.sap.sse.common.settings.generic.SettingsList;
import com.sap.sse.common.settings.generic.StringSetting;
import com.sap.sse.common.settings.generic.UUIDSetting;
public class TrackedEventPreference extends AbstractGenericSerializableSettings {
private static final long serialVersionUID = 234711768869820003L;
private transient UUIDSetting eventId;
private transient StringSetting leaderboardName;
private transient StringSetting baseUrl;
private transient BooleanSetting isArchived;
private transient StringSetting regattaSecret;
private transient SettingsList<TrackedElementWithDeviceId> trackedElements;
public TrackedEventPreference() {
}
/** copy constructor */
public TrackedEventPreference(TrackedEventPreference other) {
this();
eventId.setValue(other.getEventId());
leaderboardName.setValue(other.getLeaderboardName());
trackedElements.setValues(other.getTrackedElements());
baseUrl.setValue(other.getBaseUrl());
isArchived.setValue(other.getIsArchived());
this.regattaSecret.setValue(other.getRegattaSecret());
}
/** copy constructor with new archived state */
public TrackedEventPreference(TrackedEventPreference other, boolean isArchived) {
this();
eventId.setValue(other.getEventId());
leaderboardName.setValue(other.getLeaderboardName());
trackedElements.setValues(other.getTrackedElements());
baseUrl.setValue(other.getBaseUrl());
this.isArchived.setValue(isArchived);
this.regattaSecret.setValue(other.getRegattaSecret());
}
/** copy constructor with additional tracked event */
public TrackedEventPreference(TrackedEventPreference other, TrackedElementWithDeviceId trackedElement) {
this();
eventId.setValue(other.getEventId());
leaderboardName.setValue(other.getLeaderboardName());
final Collection<TrackedElementWithDeviceId> trackedElements = new HashSet<>();
Util.addAll(other.getTrackedElements(), trackedElements);
trackedElements.add(trackedElement);
this.trackedElements.setValues(trackedElements);
baseUrl.setValue(other.getBaseUrl());
this.isArchived.setValue(other.getIsArchived());
this.regattaSecret.setValue(other.getRegattaSecret());
}
public TrackedEventPreference(UUID eventId, String leaderboardName,
Iterable<TrackedElementWithDeviceId> trackedElements, String baseUrl, boolean isArchived,
String regattaSecret) {
this();
this.eventId.setValue(eventId);
this.leaderboardName.setValue(leaderboardName);
this.trackedElements.setValues(trackedElements);
this.baseUrl.setValue(baseUrl);
this.isArchived.setValue(isArchived);
this.regattaSecret.setValue(regattaSecret);
}
@Override
protected void addChildSettings() {
eventId = new UUIDSetting("eventId", this);
leaderboardName = new StringSetting("leaderboardName", this);
trackedElements = new SettingsList<TrackedElementWithDeviceId>("trackedElements", this,
() -> new TrackedElementWithDeviceId());
baseUrl = new StringSetting("baseUrl", this);
isArchived = new BooleanSetting("isArchived", this);
regattaSecret = new StringSetting("regattaSecret", this);
}
public UUID getEventId() {
return eventId.getValue();
}
public String getLeaderboardName() {
return leaderboardName.getValue();
}
public Iterable<TrackedElementWithDeviceId> getTrackedElements() {
return trackedElements.getValues();
}
public String getBaseUrl() {
return baseUrl.getValue();
}
public boolean getIsArchived() {
return isArchived.getValue();
}
public String getRegattaSecret() {
return regattaSecret.getValue();
}
}
@@ -0,0 +1,30 @@
package com.sap.sailing.server.impl.preferences.model;
import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings;
import com.sap.sse.common.settings.generic.SettingsList;
public class TrackedEventPreferences extends AbstractGenericSerializableSettings {
private static final long serialVersionUID = -290209497308646446L;
// TODO: setting preference title
public static final String PREF_NAME = SailingPreferences.TRACKED_EVENTS_PREFERENCES;
private transient SettingsList<TrackedEventPreference> trackedEvents;
public TrackedEventPreferences() {
}
@Override
protected void addChildSettings() {
trackedEvents = new SettingsList<>("trackedEvents", this, TrackedEventPreference::new);
}
public Iterable<TrackedEventPreference> getTrackedEvents() {
return trackedEvents.getValues();
}
public void setTrackedEvents(Iterable<TrackedEventPreference> trackedEvents) {
this.trackedEvents.setValues(trackedEvents);
}
}
@@ -21,6 +21,12 @@
<div class="mainContent">
<h2 class="releaseHeadline">Release Notes - Administration Console</h2>
<div class="innerContent">
<h2 class="articleSubheadline">July 2019</h2>
<ul class="bulletList">
<li>Result and competitor import URLs are now only considered when the requesting user
has the permission to read that URL.</li>
</ul>
<h2 class="articleSubheadline">June 2019</h2>
<ul class="bulletList">
<li>Added new API endpoints for creating, reading, updating and deleting roles under /restsecurity/role.
@@ -35,7 +41,12 @@
<li>Blue flag up/down times can be cleared in the AdminConsole now</li>
<li>Users can now create media tracks if they have the permissions based on the ownership that
will result from the creation process.</li>
<li>A couple of new API end points for managing tracked events were added under /api/v1/trackedevents/.
The new end points support creation, reading, updating and deleting tracked events and their
associated device-competitor/boat/mark elements for the current user.
</li>
</ul>
<h2 class="articleSubheadline">May 2019</h2>
<ul class="bulletList">
<li>Ranks (finishing orders) entered by the Race Manager app user are no longer
@@ -0,0 +1,114 @@
package com.sap.sse.common.impl;
import com.sap.sse.common.Duration;
/**
* Uses double for internal representation. Therefore this implementation achieves a higher accuracy but uses more
* memory in comparison to {@link MillisecondsDurationImpl}.
*
* @author Daniel Lisunkin (i505543)
*
*/
public class SecondsDurationImpl implements Duration {
private static final long serialVersionUID = 7575912908654324216L;
private final double seconds;
public SecondsDurationImpl(double seconds) {
super();
this.seconds = seconds;
}
@Override
public int compareTo(Duration o) {
double diff = seconds - o.asSeconds();
return diff > 0. ? 1 : diff < 0. ? -1 : 0;
}
@Override
public long asMillis() {
return Math.round(seconds / Duration.ONE_MILLISECOND.asSeconds());
}
@Override
public double asSeconds() {
return seconds;
}
@Override
public double asMinutes() {
return seconds / Duration.ONE_MINUTE.asSeconds();
}
@Override
public double asHours() {
return seconds / Duration.ONE_HOUR.asSeconds();
}
@Override
public double asDays() {
return seconds / Duration.ONE_DAY.asSeconds();
}
@Override
public Duration abs() {
return seconds >= 0 ? this : new SecondsDurationImpl(-seconds);
}
@Override
public Duration divide(long divisor) {
return new SecondsDurationImpl(seconds / divisor);
}
@Override
public Duration divide(double divisor) {
return new SecondsDurationImpl(seconds / divisor);
}
@Override
public double divide(Duration duration) {
return (seconds / duration.asSeconds());
}
@Override
public Duration times(long factor) {
return new SecondsDurationImpl(seconds * factor);
}
@Override
public Duration times(double factor) {
return new SecondsDurationImpl(seconds * factor);
}
@Override
public Duration minus(Duration duration) {
return new SecondsDurationImpl(seconds - duration.asSeconds());
}
@Override
public Duration minus(long milliseconds) {
return new SecondsDurationImpl(seconds - (double) milliseconds / 1000.);
}
@Override
public Duration plus(long milliseconds) {
return new SecondsDurationImpl(seconds + milliseconds / 1000.);
}
@Override
public Duration plus(Duration duration) {
return new SecondsDurationImpl(seconds + duration.asSeconds());
}
@Override
public Duration mod(Duration d) {
return new SecondsDurationImpl(seconds % d.asSeconds());
}
@Override
public String toString() {
StringBuffer result = new StringBuffer();
result.append(asSeconds()).append("s");
return result.toString();
}
}
@@ -4,6 +4,8 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.sap.sse.common.Util;
/**
* A group of users; equality and hash code are based solely on the {@link #getId() ID}.
*/
@@ -45,7 +47,6 @@ public abstract class AbstractUserGroupImpl<U extends UserReference, RD extends
@Override
public String toString() {
return "AbstractUserGroupImpl [users=" + users + ", getName()=" + getName() + ", getId()=" + getId()
+ ", getRoleDefinitionMap()=" + getRoleDefinitionMap() + "]";
return "User group "+getName()+", "+Util.size(getUsers())+" users";
}
}
@@ -116,9 +116,6 @@
<bool name="preference_allow_edit_race_factor_default">true</bool>
<string name="preference_protest_time_duration_key">protestTimeDuration</string>
<integer name="preference_protest_time_duration_default">90</integer>
<string name="preference_protest_time_duration_custom_key">protestTimeDurationCustom</string>
<string name="preference_protest_time_duration_custom_editable_key">protestTimeDurationCustomEditable</string>
<bool name="preference_protest_time_duration_custom_editable_default">true</bool>
<string name="preference_allow_demo_key" translatable="false">allow_demo</string>
<bool name="preference_allow_demo_default">false</bool>
@@ -523,33 +523,12 @@ public class AppPreferences {
context.getResources().getInteger(R.integer.preference_protest_time_duration_default));
}
public int getProtestTimeDurationInMinutesCustom() {
return helper.getInt(context.getString(R.string.preference_protest_time_duration_custom_key), -1);
}
public boolean isDefaultProtestTimeCustomEditable() {
return helper.getBoolean(context.getString(R.string.preference_protest_time_duration_custom_editable_key),
context.getResources().getBoolean(R.bool.preference_protest_time_duration_custom_editable_default));
}
public void setDefaultProtestTimeDurationInMinutes(int protestTimeInMinutes) {
helper.getEditor()
.putInt(context.getString(R.string.preference_protest_time_duration_key), protestTimeInMinutes)
.commit();
}
public void setDefaultProtestTimeDurationInMinutesCustom(int protestTimeInMinutest) {
helper.getEditor()
.putInt(context.getString(R.string.preference_protest_time_duration_custom_key), protestTimeInMinutest)
.commit();
}
public void setDefaultProtestTimeDurationInMinutesCustomEditable(boolean editable) {
helper.getEditor()
.putBoolean(context.getString(R.string.preference_protest_time_duration_custom_editable_key), editable)
.commit();
}
public void setAccessToken(String accessToken) {
helper.getEditor().putString(context.getString(R.string.preference_access_token_key), accessToken).commit();
}
@@ -1,8 +1,5 @@
package com.sap.sailing.racecommittee.app.domain.configuration.impl;
import java.util.ArrayList;
import java.util.HashSet;
import com.sap.sailing.android.shared.logging.ExLog;
import com.sap.sailing.domain.base.configuration.ConfigurationLoader;
import com.sap.sailing.domain.base.configuration.RacingProcedureConfiguration;
@@ -23,6 +20,9 @@ import com.sap.sailing.domain.common.racelog.RacingProcedureType;
import com.sap.sailing.racecommittee.app.AppPreferences;
import com.sap.sse.common.Duration;
import java.util.ArrayList;
import java.util.HashSet;
public class PreferencesRegattaConfigurationLoader implements ConfigurationLoader<RegattaConfiguration> {
private static final String TAG = PreferencesRegattaConfigurationLoader.class.getSimpleName();
@@ -113,9 +113,6 @@ public class PreferencesRegattaConfigurationLoader implements ConfigurationLoade
if (configuration.getDefaultProtestTimeDuration() != null) {
preferences.setDefaultProtestTimeDurationInMinutes(
(int) configuration.getDefaultProtestTimeDuration().asMinutes());
preferences.setDefaultProtestTimeDurationInMinutesCustomEditable(false);
} else {
preferences.setDefaultProtestTimeDurationInMinutesCustomEditable(true);
}
if (configuration.getRRS26Configuration() != null) {
@@ -401,7 +401,6 @@ public class LoginActivity extends BaseActivity implements EventSelectedListener
public void onLoadFailed(Exception reason) {
dismissProgressSpinner();
preferences.setDefaultProtestTimeDurationInMinutesCustomEditable(true);
if (reason instanceof FileNotFoundException) {
Toast.makeText(getApplicationContext(), getString(R.string.loading_configuration_not_found),
Toast.LENGTH_LONG).show();
@@ -319,8 +319,7 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements
TimePoint startTime = TimeUtils.getTime(mTimePicker);
TimePoint now = MillisecondsTimePoint.now();
for (ManagedRace race : selectedRaces) {
Duration duration = Duration.ONE_MINUTE
.times(AppPreferences.on(getActivity()).getProtestTimeDurationInMinutes());
Duration duration = Duration.ONE_MINUTE.times(mDuration);
TimeRange protestTime = new TimeRangeImpl(startTime, startTime.plus(duration));
race.getState().setProtestTime(now, protestTime);
}
@@ -404,12 +403,6 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements
private void updateProtestRange() {
if (mDuration == null) {
mDuration = mPreferences.getProtestTimeDurationInMinutes();
if (mPreferences.isDefaultProtestTimeCustomEditable()) {
int custom = mPreferences.getProtestTimeDurationInMinutesCustom();
if (custom >= 0) {
mDuration = custom;
}
}
}
if (mProtestDuration != null) {
mProtestDuration.setText(getString(R.string.protest_duration, mDuration));
@@ -468,9 +461,9 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mDuration = Integer.parseInt(duration.getText().toString());
if (mPreferences.isDefaultProtestTimeCustomEditable()) {
mPreferences.setDefaultProtestTimeDurationInMinutesCustom(mDuration);
try {
mDuration = Integer.parseInt(duration.getText().toString());
} catch (NumberFormatException ignored) {
}
updateProtestRange();
}
@@ -33,6 +33,7 @@ import com.sap.sailing.racecommittee.app.utils.autoupdate.AutoUpdater;
import java.io.FileNotFoundException;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public class GeneralPreferenceFragment extends BasePreferenceFragment {
@@ -200,6 +201,7 @@ public class GeneralPreferenceFragment extends BasePreferenceFragment {
//Update all the other preferences, e.g. course areas, mail recipient
final SharedPreferences.OnSharedPreferenceChangeListener listener =
new SharedPreferences.OnSharedPreferenceChangeListener() {
@SuppressWarnings("unchecked")
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
final Preference preference = findPreference(key);
@@ -211,6 +213,9 @@ public class GeneralPreferenceFragment extends BasePreferenceFragment {
if (listener != null) {
listener.onPreferenceChange(preference, newValue);
}
if (preference instanceof EditSetPreference) {
preference.persistStringSet((Set<String>) newValue);
}
}
};
@@ -25,6 +25,10 @@ public class IntegerEditTextPreference extends EditTextPreference {
@Override
protected boolean persistString(String value) {
return persistInt(Integer.valueOf(value));
try {
return persistInt(Integer.valueOf(value));
} catch (NumberFormatException e) {
return false;
}
}
}