diff --git a/configuration/000-macros.conf b/configuration/000-macros.conf index cee541ab0eb..0d7421cac38 100755 --- a/configuration/000-macros.conf +++ b/configuration/000-macros.conf @@ -155,7 +155,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "http://$HOSTNAME/dashboards/RibDashboard.html?leaderboardName=$LEADERBOARD_NAME&%{QUERY_STRING}" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "http://$HOSTNAME/dashboards/RibDashboard.html?leaderboardName=$LEADERBOARD_NAME&%1" [L,NE] Use Rewrite $IP $PORT @@ -216,7 +218,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html?%{QUERY_STRING}#/event/:eventId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html?%1#/event/:eventId=$EVENTUUID" [L,NE] Use Rewrite $IP $PORT @@ -236,7 +240,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "https://$HOSTNAME/gwt/Home.html?%{QUERY_STRING}#/event/:eventId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "https://$HOSTNAME/gwt/Home.html?%1#/event/:eventId=$EVENTUUID" [L,NE] Use Rewrite $IP $PORT @@ -251,7 +257,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html?%{QUERY_STRING}#/series/:seriesId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html?%1#/series/:seriesId=$EVENTUUID" [L,NE] Use Rewrite $IP $PORT @@ -271,7 +279,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "https://$HOSTNAME/gwt/Home.html?%{QUERY_STRING}#/series/:seriesId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "https://$HOSTNAME/gwt/Home.html?%1#/series/:seriesId=$EVENTUUID" [L,NE] Use Rewrite $IP $PORT @@ -286,7 +296,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html#/regatta/overview/:eventId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html?%1#/regatta/overview/:eventId=$EVENTUUID" [L,NE] Use Rewrite $IP $PORT @@ -316,7 +328,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html#/event/:eventId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/Home.html?%1#/event/:eventId=$EVENTUUID" [L,NE] Use Rewrite 172.31.30.89 8888 @@ -336,7 +350,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "https://$HOSTNAME/gwt/Home.html#/event/:eventId=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "https://$HOSTNAME/gwt/Home.html?%1#/event/:eventId=$EVENTUUID" [L,NE] Use Rewrite 172.31.30.89 8888 @@ -352,7 +368,9 @@ RewriteEngine on RewriteCond %{REQUEST_URI} "^(/)?$" - RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/RegattaOverview.html?event=$EVENTUUID" [L,NE] + # The following matches the query string and captures it in group %1 for the RewriteRule to use before the fragment + RewriteCond %{QUERY_STRING} "^(.*)$" + RewriteRule ^(/)?$ "http://$HOSTNAME/gwt/RegattaOverview.html?event=$EVENTUUID&%1" [L,NE] Use Rewrite $IP $PORT @@ -556,4 +574,4 @@ Use Rewrite 172.31.18.218 8888 - \ No newline at end of file + diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java index 0388c3acb7b..57777951acf 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java @@ -218,6 +218,21 @@ public enum BoatClassMasterdata { public boolean hasAdditionalDownwindSail() { return hasAdditionalDownwindSail; } + + /** + * Gets the list of names for this {@link BoatClassMasterdata} instance, containing its + * {@link #displayName display name} and the {@link #alternativeNames alternative names} (if any). + * + * @return the list of names for the boat class + */ + public Iterable getBoatClassNames() { + List result = new ArrayList<>(getAlternativeNames().length + 1); + result.add(this.getDisplayName()); + for (String alternativeName : this.getAlternativeNames()) { + result.add(alternativeName); + } + return result; + } public static Iterable getAllBoatClassNames(boolean includeAlternativeNames) { final List result = new ArrayList<>(); diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java old mode 100755 new mode 100644 index a07c883e36f..4282ada013f --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BoatClassDTO.java @@ -2,7 +2,7 @@ package com.sap.sailing.domain.common.dto; import java.io.Serializable; -public class BoatClassDTO extends NamedDTO implements Serializable { +public class BoatClassDTO extends NamedDTO implements Serializable, Comparable { private static final long serialVersionUID = 1981789833769906676L; /** @@ -34,4 +34,9 @@ public class BoatClassDTO extends NamedDTO implements Serializable { public String getDisplayName() { return displayName; } + + @Override + public int compareTo(BoatClassDTO o) { + return getName().compareToIgnoreCase(o.getName()); + } } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 1381b56d115..27b784d3456 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -1033,6 +1033,14 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { logger.severe("Error parsing official website URL "+officialWebSiteURLAsString+" for event "+name+". Ignoring this URL."); } } + String baseURLAsString = (String) eventDBObject.get(FieldNames.EVENT_BASE_URL.name()); + if (baseURLAsString != null) { + try { + result.setBaseURL(new URL(baseURLAsString)); + } catch (MalformedURLException e) { + logger.severe("Error parsing base URL "+baseURLAsString+" for event "+name+". Ignoring this URL."); + } + } BasicDBList images = (BasicDBList) eventDBObject.get(FieldNames.EVENT_IMAGES.name()); if (images != null) { for (Object imageObject : images) { diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java index 8d25d944215..319e4a385b3 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java @@ -22,6 +22,7 @@ public enum FieldNames { EVENT_IMAGES, EVENT_VIDEOS, EVENT_OFFICIAL_WEBSITE_URL, + EVENT_BASE_URL, EVENT_SAILORS_INFO_WEBSITE_URL, EVENT_SAILORS_INFO_WEBSITES, EVENT_LOGO_IMAGE_URL, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index 6fc98aac781..8c10c04d2e4 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -550,6 +550,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { eventDBObject.put(FieldNames.EVENT_DESCRIPTION.name(), event.getDescription()); eventDBObject.put(FieldNames.EVENT_ID.name(), event.getId()); eventDBObject.put(FieldNames.EVENT_OFFICIAL_WEBSITE_URL.name(), event.getOfficialWebsiteURL() != null ? event.getOfficialWebsiteURL().toString() : null); + eventDBObject.put(FieldNames.EVENT_BASE_URL.name(), event.getBaseURL() != null ? event.getBaseURL().toString() : null); storeTimePoint(event.getStartDate(), eventDBObject, FieldNames.EVENT_START_DATE); storeTimePoint(event.getEndDate(), eventDBObject, FieldNames.EVENT_END_DATE); eventDBObject.put(FieldNames.EVENT_IS_PUBLIC.name(), event.isPublic()); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java index ad3cd38f7ff..59efc7d8b80 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java @@ -49,6 +49,16 @@ public interface EventBase extends NamedWithID, WithDescription, Renamable, With void setOfficialWebsiteURL(URL officialWebsiteURL); + /** + * @return the URL under which the event's landing page in the {@code sapsailing.com} universe can be reached. This + * could be something like {@code tw2015.sapsailing.com} or {@link 505worlds2012.sapsailing.com} or similar. + * It shall be a URL to which relative paths to pages in the context of this event can be appended, such + * as links to race boards, leaderboards or the regatta overview. + */ + URL getBaseURL(); + + void setBaseURL(URL baseURL); + /** * Returns a mapping of Locales to URLs where the URLs are meant as an external web site containing sailor related * information like protests, official results, etc. diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java index ef8092abecc..84d0b07641d 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java @@ -38,6 +38,7 @@ public abstract class EventBaseImpl implements EventBase { private TimePoint startDate; private TimePoint endDate; private URL officialWebsiteURL; + private URL baseURL; private CopyOnWriteHashMap sailorsInfoWebsiteURLs; private ConcurrentLinkedQueue images; private ConcurrentLinkedQueue videos; @@ -144,12 +145,22 @@ public abstract class EventBaseImpl implements EventBase { public URL getOfficialWebsiteURL() { return officialWebsiteURL; } - + @Override public void setOfficialWebsiteURL(URL officialWebsiteURL) { this.officialWebsiteURL = officialWebsiteURL; } + @Override + public URL getBaseURL() { + return baseURL; + } + + @Override + public void setBaseURL(URL baseURL) { + this.baseURL = baseURL; + } + @Override public Map getSailorsInfoWebsiteURLs() { return Collections.unmodifiableMap(sailorsInfoWebsiteURLs); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index 2ce3c3f76ae..6d721a5e89b 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -612,18 +612,18 @@ public class DomainFactoryImpl implements DomainFactory { @Override public BoatClass getDominantBoatClass(Iterable competitorClassNames) { + final BoatClass result; if (competitorClassNames == null) { - return null; - + result = null; } else { Collection boatClasses = new ArrayList<>(); for (String competitorClassName : competitorClassNames) { BoatClass boatClass = getOrCreateBoatClass(competitorClassName); boatClasses.add(boatClass); } - - return Util.getDominantObject(boatClasses); + result = Util.getDominantObject(boatClasses); } + return result; } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java index d0635587a1f..f8ba35fa65c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseArea; import com.sap.sailing.domain.base.DomainFactory; @@ -28,6 +29,7 @@ import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; /** * A leaderboard is used to display the results of one or more {@link TrackedRace races}. It manages the competitors' @@ -532,4 +534,18 @@ public interface Leaderboard extends LeaderboardBase, HasRaceColumns { NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher(); + /** + * Looks through all {@link #getRaceColumns() race columns} and their {@link RaceColumn#getFleets() fleets} and checks + * if {@code trackedRace} is {@link RaceColumn#getTrackedRace(Fleet) linked} to that combination. If such a slot is found + * that "slot" is returned by a pair specifying the non-{@code null} {@link RaceColumn} and {@code Fleet} pair. Otherwise, + * {@code null} is returned. + */ + Pair getRaceColumnAndFleet(TrackedRace trackedRace); + + /** + * Gets the ("dominant") boat class for this leaderboard. For a {@link RegattaLeaderboard} this is the {@link Regatta}'s boat class. + * For a {@link FlexibleLeaderboard} the implementation is more complex because no fixed boat class is set for the leaderboard. There, + * the boat class will be determined based on the most frequently occurring boat class when iterating across the competitors. + */ + BoatClass getBoatClass(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java index 2dd3fc8375c..a4ba3c0c0ec 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java @@ -160,8 +160,8 @@ public class LeaderboardCacheManager { } @Override - public void maxPointsReasonChanced(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { + public void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { removeFromCache(leaderboard); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java index 0cdf41e2f3f..eb64022400d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java @@ -8,7 +8,7 @@ import com.sap.sse.common.TimePoint; public interface ScoreCorrectionListener { void correctedScoreChanced(Competitor competitor, RaceColumn raceColumn, Double oldCorrectedScore, Double newCorrectedScore); - void maxPointsReasonChanced(Competitor competitor, MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason); + void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason); void carriedPointsChanged(Competitor competitor, Double oldCarriedPoints, Double newCarriedPoints); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java index 7d5cad8055b..10b1eaeb283 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java @@ -6,6 +6,10 @@ import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sse.common.TimePoint; public interface SettableScoreCorrection extends ScoreCorrection { + /** + * The listeners added are transient and do not need to be {@link Serializable}. When this object is + * de-serialized, it starts out with an empty set of listeners. + */ void addScoreCorrectionListener(ScoreCorrectionListener listener); void removeScoreCorrectionListener(ScoreCorrectionListener listener); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index 30894929bd4..e6833b076e3 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -29,10 +29,13 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.DomainFactory; @@ -93,6 +96,7 @@ import com.sap.sailing.util.impl.RaceColumnListeners; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; @@ -1938,6 +1942,25 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race return new NumberOfCompetitorsFetcherImpl(); } + @Override + public Pair getRaceColumnAndFleet(TrackedRace trackedRace) { + for (final RaceColumn raceColumn : getRaceColumns()) { + for (final Fleet fleet : raceColumn.getFleets()) { + if (raceColumn.getTrackedRace(fleet) == trackedRace) { + return new Pair<>(raceColumn, fleet); + } + } + } + return null; + } + + + @Override + public BoatClass getBoatClass() { + return Util.getDominantObject(StreamSupport.stream(getCompetitors().spliterator(), /* parallel */ false). + map(c->c.getBoat().getBoatClass()).collect(Collectors.toList())); + } + protected abstract LeaderboardType getLeaderboardType(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java index d5af239cf6a..72cfd8ca4d4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseArea; import com.sap.sailing.domain.base.Fleet; @@ -53,10 +54,14 @@ public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements R public Regatta getRegatta() { return regatta; } - + + public static String getLeaderboardNameForRegatta(Regatta regatta) { + return regatta.getName(); + } + @Override public String getName() { - return getRegatta().getName(); + return getLeaderboardNameForRegatta(getRegatta()); } @Override @@ -120,4 +125,9 @@ public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements R protected LeaderboardType getLeaderboardType() { return LeaderboardType.RegattaLeaderboard; } + + @Override + public BoatClass getBoatClass() { + return getRegatta().getBoatClass(); + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java index 8b2e2d59ca4..47d1e4fb489 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java @@ -103,10 +103,10 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { } } - protected void notifyListeners(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { + protected void notifyListeners(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { for (ScoreCorrectionListener listener : getScoreCorrectionListeners()) { - listener.maxPointsReasonChanced(competitor, oldMaxPointsReason, newMaxPointsReason); + listener.maxPointsReasonChanced(competitor, raceColumn, oldMaxPointsReason, newMaxPointsReason); } } @@ -148,7 +148,7 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { } else { oldMaxPointsReason = maxPointsReasons.put(key, reason); } - notifyListeners(competitor, oldMaxPointsReason, reason); + notifyListeners(competitor, raceColumn, oldMaxPointsReason, reason); } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java index 030b22f75db..460ab3af697 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java @@ -74,9 +74,9 @@ public abstract class AbstractMetaLeaderboard extends AbstractSimpleLeaderboardI } @Override - public void maxPointsReasonChanced(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { - getScoreCorrection().notifyListeners(competitor, oldMaxPointsReason, newMaxPointsReason); + public void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { + getScoreCorrection().notifyListeners(competitor, raceColumn, oldMaxPointsReason, newMaxPointsReason); } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java index 11206d8011e..7e5643534eb 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java @@ -36,9 +36,9 @@ public class MetaLeaderboardScoreCorrection extends ScoreCorrectionImpl { * These redefinitions are required for scoping reasons, to make the method visible also to other classes in this package. */ @Override - protected void notifyListeners(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { - super.notifyListeners(competitor, oldMaxPointsReason, newMaxPointsReason); + protected void notifyListeners(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { + super.notifyListeners(competitor, raceColumn, oldMaxPointsReason, newMaxPointsReason); } @Override diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java index e6383c266c8..3a3c4a156a0 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java @@ -206,7 +206,7 @@ public class GwtTestCaseColumnToggling extends GWTTestCase { }); } - private void removeColumnAndAssert(){ + private void removeColumnAndAssert() { service.removeLeaderboardColumn(LEADERBOARD_NAME, COLUMN1_NAME, new AsyncCallback() { @Override @@ -220,7 +220,7 @@ public class GwtTestCaseColumnToggling extends GWTTestCase { leaderboardPanel.updateLeaderboard(leaderboard); assertNotNull(rc); try { - rc.toggleExpansion(); + rc.changeExpansionState(/* expand */ !rc.isExpanded()); leaderboardPanel.updateLeaderboard(leaderboard); } catch (Exception e) { fail("Toggle column failed." + e.getLocalizedMessage()); diff --git a/java/com.sap.sailing.gwt.ui/EmbeddedMapAndWindChart.css b/java/com.sap.sailing.gwt.ui/EmbeddedMapAndWindChart.css index 6376c253b35..e5a55d6ef7a 100644 --- a/java/com.sap.sailing.gwt.ui/EmbeddedMapAndWindChart.css +++ b/java/com.sap.sailing.gwt.ui/EmbeddedMapAndWindChart.css @@ -78,16 +78,11 @@ table { border-collapse: separate; } -.CombinedWindPanelParentDiv { +.TopLeftControlsWrapperPanelParentDiv { padding-top: 34px; padding-left: 10px; } -.TrueNorthIndicatorPanelParentDiv { - padding-top: 10px; - padding-left: 10px; -} - .MapSimulationLegendParentDiv { padding-bottom: 10px; padding-right: 10px; diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 4842be0c7c1..bf79c662db7 100755 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -85,21 +85,15 @@ table { border-collapse: separate; } -.CombinedWindPanelParentDiv { +.TopLeftControlsWrapperPanelParentDiv { padding-top: 34px; padding-left: 10px; } -.TrueNorthIndicatorPanelParentDiv { - padding-top: 10px; - padding-left: 10px; -} -.CombinedWindPanelParentDiv.indentsmall, -.TrueNorthIndicatorPanelParentDiv.indentsmall { +.TopLeftControlsWrapperPanel.indentsmall { padding-left: 30px; } -.CombinedWindPanelParentDiv.indentbig, -.TrueNorthIndicatorPanelParentDiv.indentbig { +.TopLeftControlsWrapperPanel.indentbig { padding-left: 45px; } diff --git a/java/com.sap.sailing.gwt.ui/Simulator.css b/java/com.sap.sailing.gwt.ui/Simulator.css index 14581efa50f..8b45204abf0 100644 --- a/java/com.sap.sailing.gwt.ui/Simulator.css +++ b/java/com.sap.sailing.gwt.ui/Simulator.css @@ -494,16 +494,11 @@ table { border-collapse: separate; } -.CombinedWindPanelParentDiv { +.TopLeftControlsWrapperPanelParentDiv { padding-top: 34px; padding-left: 10px; } -.TrueNorthIndicatorPanelParentDiv { - padding-top: 10px; - padding-left: 10px; -} - .MapSimulationLegendParentDiv { padding-bottom: 10px; padding-right: 10px; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java index 3ec805f6f37..e716d2100d0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/start/DesktopStartView.java @@ -27,7 +27,6 @@ import com.sap.sailing.gwt.autoplay.client.app.PlaceNavigator; import com.sap.sailing.gwt.autoplay.client.place.player.AutoPlayerConfiguration; import com.sap.sailing.gwt.autoplay.client.shared.leaderboard.LeaderboardWithHeaderPerspectiveLifecycle; import com.sap.sailing.gwt.autoplay.client.shared.leaderboard.LeaderboardWithHeaderPerspectiveSettings; -import com.sap.sailing.gwt.common.client.GWTLocaleUtil; import com.sap.sailing.gwt.common.client.SharedResources; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.raceboard.RaceBoardPerspectiveLifecycle; @@ -36,6 +35,7 @@ import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sse.common.settings.Settings; +import com.sap.sse.gwt.client.GWTLocaleUtil; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.event.LocaleChangeEvent; import com.sap.sse.gwt.client.sapheader.SAPHeader; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/Home.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/Home.gwt.xml index a6a8c57ea96..0639d74a28a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/Home.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/Home.gwt.xml @@ -45,6 +45,7 @@ + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java index f17c2ba6bb3..9be312e0c4d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchContext.java @@ -28,4 +28,7 @@ public interface SailingDispatchContext extends DispatchContext { @GwtIncompatible Date getCurrentClientTime(); + T getPreferenceForCurrentUser(String preferenceKey); + + void setPreferenceForCurrentUser(String preferenceKey, Object preference); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorDTO.java index 1a05fa966cd..ebde9a2c6af 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorDTO.java @@ -8,8 +8,9 @@ import com.sap.sailing.domain.base.Nationality; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.NamedDTO; import com.sap.sse.common.CountryCode; +import com.sap.sse.gwt.dispatch.shared.commands.DTO; -public class SimpleCompetitorDTO extends NamedDTO implements Serializable, Comparable { +public class SimpleCompetitorDTO extends NamedDTO implements DTO, Serializable, Comparable { private static final long serialVersionUID = -5743976446085202047L; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorWithIdDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorWithIdDTO.java new file mode 100644 index 00000000000..21860294e5e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/SimpleCompetitorWithIdDTO.java @@ -0,0 +1,45 @@ +package com.sap.sailing.gwt.home.communication.event; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.dto.CompetitorDTO; + +public class SimpleCompetitorWithIdDTO extends SimpleCompetitorDTO { + + private static final long serialVersionUID = -1236159499763467614L; + + private String idAsString; + + protected SimpleCompetitorWithIdDTO() { + } + + @GwtIncompatible + public SimpleCompetitorWithIdDTO(Competitor competitor) { + super(competitor); + this.idAsString = competitor.getId().toString(); + } + + @GwtIncompatible + public SimpleCompetitorWithIdDTO(CompetitorDTO competitor) { + super(competitor); + this.idAsString = competitor.getIdAsString(); + } + + public SimpleCompetitorWithIdDTO(String idAsString, String name, String sailID, + String twoLetterIsoCountryCode, String flagImageURL) { + super(name, sailID, twoLetterIsoCountryCode, flagImageURL); + this.idAsString = idAsString; + } + + public String getIdAsString() { + return idAsString; + } + + @Override + public int compareTo(SimpleCompetitorDTO obj) { + int compareTo = super.compareTo(obj); + return (compareTo == 0 && obj instanceof SimpleCompetitorWithIdDTO) + ? this.idAsString.compareTo(((SimpleCompetitorWithIdDTO) obj).idAsString) : compareTo; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/CompetitorSuggestionResult.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/CompetitorSuggestionResult.java new file mode 100644 index 00000000000..3d15aa2eda0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/CompetitorSuggestionResult.java @@ -0,0 +1,39 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.Collection; + +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sse.gwt.dispatch.shared.commands.SortedSetResult; + +/** + * {@link SortedSetResult} extension for suggested competitor search. This result contains a set of + * {@link SimpleCompetitorWithIdDTO competitor}s representing the suggestions, as well as information if there are more + * suggestions possible. + */ +public class CompetitorSuggestionResult extends SortedSetResult { + + private int moreSuggestionsCount; + + protected CompetitorSuggestionResult() { + } + + public CompetitorSuggestionResult(Collection values, int moreSuggestionsCount) { + super(values); + this.moreSuggestionsCount = moreSuggestionsCount; + } + + /** + * @return the amount of additionally possible suggestions. + */ + public int getMoreSuggestionsCount() { + return moreSuggestionsCount; + } + + /** + * @return true if there is at least one {@link #getMoreSuggestionsCount() additional suggestion + * possible}, false otherwise + */ + public boolean hasMoreSuggestions() { + return moreSuggestionsCount > 0; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoriteBoatClassesDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoriteBoatClassesDTO.java new file mode 100644 index 00000000000..4cff5cfe15b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoriteBoatClassesDTO.java @@ -0,0 +1,39 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.ArrayList; +import java.util.Collection; + +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sse.gwt.dispatch.shared.commands.DTO; + +/** + * {@link DTO} implementation representing the favorite boat class preferences. + */ +public class FavoriteBoatClassesDTO implements DTO { + + private ArrayList selectedBoatClasses = new ArrayList<>(); + private boolean notifyAboutUpcomingRaces; + private boolean notifyAboutResults; + + protected FavoriteBoatClassesDTO() {} + + public FavoriteBoatClassesDTO(Collection selectedBoatClasses, boolean notifyAboutUpcomingRaces, + boolean notifyAboutResults) { + this.selectedBoatClasses.addAll(selectedBoatClasses); + this.notifyAboutUpcomingRaces = notifyAboutUpcomingRaces; + this.notifyAboutResults = notifyAboutResults; + } + + public Collection getSelectedBoatClasses() { + return selectedBoatClasses; + } + + public boolean isNotifyAboutUpcomingRaces() { + return notifyAboutUpcomingRaces; + } + + public boolean isNotifyAboutResults() { + return notifyAboutResults; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoriteCompetitorsDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoriteCompetitorsDTO.java new file mode 100644 index 00000000000..e53932552ba --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoriteCompetitorsDTO.java @@ -0,0 +1,33 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.ArrayList; +import java.util.Collection; + +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sse.gwt.dispatch.shared.commands.DTO; + +/** + * {@link DTO} implementation representing the favorite competitor preferences. + */ +public class FavoriteCompetitorsDTO implements DTO { + + private ArrayList selectedCompetitors = new ArrayList<>(); + private boolean notifyAboutResults; + + protected FavoriteCompetitorsDTO() {} + + public FavoriteCompetitorsDTO(Collection selectedCompetitors, + boolean notifyAboutResults) { + this.selectedCompetitors.addAll(selectedCompetitors); + this.notifyAboutResults = notifyAboutResults; + } + + public Collection getSelectedCompetitors() { + return selectedCompetitors; + } + + public boolean isNotifyAboutResults() { + return notifyAboutResults; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoritesResult.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoritesResult.java new file mode 100644 index 00000000000..81e84f99d55 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/FavoritesResult.java @@ -0,0 +1,25 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import com.sap.sse.gwt.dispatch.shared.commands.Result; + +public class FavoritesResult implements Result { + + private FavoriteBoatClassesDTO favoriteBoatClasses; + private FavoriteCompetitorsDTO favoriteCompetitors; + + protected FavoritesResult() {} + + public FavoritesResult(FavoriteBoatClassesDTO favoriteBoatClasses, FavoriteCompetitorsDTO favoriteCompetitors) { + this.favoriteBoatClasses = favoriteBoatClasses; + this.favoriteCompetitors = favoriteCompetitors; + } + + public FavoriteBoatClassesDTO getFavoriteBoatClasses() { + return favoriteBoatClasses; + } + + public FavoriteCompetitorsDTO getFavoriteCompetitors() { + return favoriteCompetitors; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetCompetitorSuggestionAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetCompetitorSuggestionAction.java new file mode 100644 index 00000000000..8b70fe591f4 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetCompetitorSuggestionAction.java @@ -0,0 +1,83 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.base.Boat; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.gwt.home.communication.SailingAction; +import com.sap.sailing.gwt.home.communication.SailingDispatchContext; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sse.common.Util; +import com.sap.sse.common.filter.AbstractListFilter; +import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException; + +/** + * {@link SailingAction} implementation to load competitor data to be shown in a suggestion box depending on the + * {@link #GetCompetitorSuggestionAction(String, int) given query tokens}, where the amount of loaded entries can be + * limited. + * + * The given query string is matched against the competitor's {@link Competitor#getName() name} and + * {@link Boat#getSailID() sail-id} (which is determined from its {@link Competitor#getBoat() boat}, if any). + */ +public class GetCompetitorSuggestionAction implements SailingAction { + + private ArrayList queryTokens; + private int limit; + + @GwtIncompatible + private final AbstractListFilter competitorFilter = new AbstractListFilter() { + @Override + public Iterable getStrings(Competitor competitor) { + if (competitor.getBoat() == null) return Arrays.asList(competitor.getName()); + return Arrays.asList(competitor.getBoat().getSailID(), competitor.getName()); + } + }; + + @SuppressWarnings("unused") + private GetCompetitorSuggestionAction() { + } + + /** + * Creates a {@link GetCompetitorSuggestionAction} instance with the given query string, where the loaded competitor + * entries are limited to the provided amount. + * + * @param queryTokens + * query tokens to load competitors for + * @param limit + * maximum number of competitor entries to be loaded + */ + public GetCompetitorSuggestionAction(Iterable queryTokens, int limit) { + this.queryTokens = (ArrayList) Util.addAll(queryTokens, new ArrayList()); + this.limit = limit; + } + + @Override + @GwtIncompatible + public CompetitorSuggestionResult execute(SailingDispatchContext ctx) throws DispatchException { + Iterable filteredCompetitors = getFilteredCompetitors(ctx); + Collection result = convertToSimpleCompetitorDTOs(filteredCompetitors); + return new CompetitorSuggestionResult(result, Math.max(0, Util.size(filteredCompetitors) - limit)); + } + + @GwtIncompatible + private Iterable getFilteredCompetitors(SailingDispatchContext ctx) { + Iterable allCompetitors = ctx.getRacingEventService().getCompetitorStore().getCompetitors(); + return competitorFilter.applyFilter(queryTokens, Util.addAll(allCompetitors, new ArrayList())); + } + + @GwtIncompatible + private Collection convertToSimpleCompetitorDTOs( + Iterable filteredCompetitors) { + Collection result = new ArrayList<>(); + int count = 0; + for (Competitor competitor : filteredCompetitors) { + result.add(new SimpleCompetitorWithIdDTO(competitor)); + if(++count >= limit) break; + } + return result; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetCompetitorsAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetCompetitorsAction.java new file mode 100644 index 00000000000..df0ffda1120 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetCompetitorsAction.java @@ -0,0 +1,50 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.ArrayList; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorStore; +import com.sap.sailing.gwt.home.communication.SailingAction; +import com.sap.sailing.gwt.home.communication.SailingDispatchContext; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sse.common.Util; +import com.sap.sse.gwt.dispatch.shared.commands.SortedSetResult; +import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException; + +/** + * {@link SailingAction} implementation to load competitor data to be shown on the preferences page for the + * {@link #GetCompetitorSuggestionAction(String, int) given ids}. + */ +public class GetCompetitorsAction implements SailingAction> { + + private ArrayList ids; + + @SuppressWarnings("unused") + private GetCompetitorsAction() { + } + + /** + * Creates a {@link GetCompetitorsAction} instance with the given ids. + * + * @param ids + * ids to load competitors for + */ + public GetCompetitorsAction(Iterable ids) { + this.ids = (ArrayList) Util.addAll(ids, new ArrayList()); + } + + @Override + @GwtIncompatible + public SortedSetResult execute(SailingDispatchContext ctx) throws DispatchException { + CompetitorStore competitorStore = ctx.getRacingEventService().getCompetitorStore(); + SortedSetResult result = new SortedSetResult<>(); + for (String id : ids) { + Competitor competitor = competitorStore.getExistingCompetitorByIdAsString(id); + result.addValue(competitor == null ? new SimpleCompetitorWithIdDTO(id, id, "", null, null) + : new SimpleCompetitorWithIdDTO(competitor)); + } + return result; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java new file mode 100644 index 00000000000..880c5ea966d --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/GetFavoritesAction.java @@ -0,0 +1,60 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.TreeSet; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.home.communication.SailingAction; +import com.sap.sailing.gwt.home.communication.SailingDispatchContext; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreference; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreferences; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreference; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences; +import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException; + +/** + * {@link SailingAction} implementation to load favorites for the currently logged in user to bee shown on the + * preferences page, preparing the appropriate data structure. + */ +public class GetFavoritesAction implements SailingAction { + + @Override + @GwtIncompatible + public FavoritesResult execute(SailingDispatchContext ctx) throws DispatchException { + return new FavoritesResult(getFavoriteBoatClasses(ctx), getFavoriteCompetitors(ctx)); + } + + @GwtIncompatible + private FavoriteBoatClassesDTO getFavoriteBoatClasses(SailingDispatchContext ctx) { + BoatClassNotificationPreferences preferences = ctx + .getPreferenceForCurrentUser(BoatClassNotificationPreferences.PREF_NAME); + TreeSet selected = new TreeSet<>(); + boolean notifyAboutUpcomingRaces = false, notifyAboutResults = false; + if (preferences != null) { + for (BoatClassNotificationPreference pref : preferences.getBoatClasses()) { + String name = pref.getBoatClass().getName(), displayName = pref.getBoatClass().getDisplayName(); + selected.add(new BoatClassDTO(name, displayName, pref.getBoatClass().getHullLength().getMeters())); + notifyAboutUpcomingRaces |= pref.isNotifyAboutUpcomingRaces(); + notifyAboutResults |= pref.isNotifyAboutResults(); + } + } + return new FavoriteBoatClassesDTO(selected, notifyAboutUpcomingRaces, notifyAboutResults); + } + + @GwtIncompatible + private FavoriteCompetitorsDTO getFavoriteCompetitors(SailingDispatchContext ctx) { + CompetitorNotificationPreferences preferences = ctx + .getPreferenceForCurrentUser(CompetitorNotificationPreferences.PREF_NAME); + TreeSet selected = new TreeSet<>(); + boolean notifyAboutResults = false; + if (preferences != null) { + for (CompetitorNotificationPreference pref : preferences.getCompetitors()) { + selected.add(new SimpleCompetitorWithIdDTO(pref.getCompetitor())); + notifyAboutResults |= pref.isNotifyAboutResults(); + } + } + return new FavoriteCompetitorsDTO(selected, notifyAboutResults); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/SaveFavoriteBoatClassesAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/SaveFavoriteBoatClassesAction.java new file mode 100644 index 00000000000..3f7bec7905e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/SaveFavoriteBoatClassesAction.java @@ -0,0 +1,47 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.home.communication.SailingAction; +import com.sap.sailing.gwt.home.communication.SailingDispatchContext; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreference; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreferences; +import com.sap.sse.gwt.dispatch.shared.commands.VoidResult; +import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException; + +/** + * {@link SailingAction} implementation to save favorite boat classes for the currently logged in user as selected on + * the preferences page. + */ +public class SaveFavoriteBoatClassesAction implements SailingAction { + + private FavoriteBoatClassesDTO favorites; + + protected SaveFavoriteBoatClassesAction() {} + + public SaveFavoriteBoatClassesAction(FavoriteBoatClassesDTO favorites) { + this.favorites = favorites; + } + + @Override + @GwtIncompatible + public VoidResult execute(SailingDispatchContext ctx) throws DispatchException { + BoatClassNotificationPreferences prefs = new BoatClassNotificationPreferences(ctx.getRacingEventService()); + List boatClassPreferences = new ArrayList<>(); + DomainFactory domainFactory = ctx.getRacingEventService().getBaseDomainFactory(); + for (BoatClassDTO boatClassDTO : favorites.getSelectedBoatClasses()) { + BoatClass boatClass = domainFactory.getOrCreateBoatClass(boatClassDTO.getName()); + boatClassPreferences.add(new BoatClassNotificationPreference(domainFactory, boatClass, + favorites.isNotifyAboutUpcomingRaces(), favorites.isNotifyAboutResults())); + } + prefs.setBoatClasses(boatClassPreferences); + ctx.setPreferenceForCurrentUser(BoatClassNotificationPreferences.PREF_NAME, prefs); + return new VoidResult(); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/SaveFavoriteCompetitorsAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/SaveFavoriteCompetitorsAction.java new file mode 100644 index 00000000000..1a7a3a8380b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/user/profile/SaveFavoriteCompetitorsAction.java @@ -0,0 +1,47 @@ +package com.sap.sailing.gwt.home.communication.user.profile; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorStore; +import com.sap.sailing.gwt.home.communication.SailingAction; +import com.sap.sailing.gwt.home.communication.SailingDispatchContext; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreference; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences; +import com.sap.sse.gwt.dispatch.shared.commands.VoidResult; +import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException; + +/** + * {@link SailingAction} implementation to save favorite competitors for the currently logged in user as selected on + * the preferences page. + */ +public class SaveFavoriteCompetitorsAction implements SailingAction { + + private FavoriteCompetitorsDTO favorites; + + protected SaveFavoriteCompetitorsAction() {} + + public SaveFavoriteCompetitorsAction(FavoriteCompetitorsDTO favorites) { + this.favorites = favorites; + } + + @Override + @GwtIncompatible + public VoidResult execute(SailingDispatchContext ctx) throws DispatchException { + CompetitorNotificationPreferences prefs = new CompetitorNotificationPreferences(ctx.getRacingEventService()); + List competitorPreferences = new ArrayList<>(); + CompetitorStore competitorStore = ctx.getRacingEventService().getCompetitorStore(); + for (SimpleCompetitorWithIdDTO competitorDTO : favorites.getSelectedCompetitors()) { + Competitor competitor = competitorStore.getExistingCompetitorByIdAsString(competitorDTO.getIdAsString()); + competitorPreferences.add(new CompetitorNotificationPreference(competitorStore, competitor, + favorites.isNotifyAboutResults())); + } + prefs.setCompetitors(competitorPreferences); + ctx.setPreferenceForCurrentUser(CompetitorNotificationPreferences.PREF_NAME, prefs); + return new VoidResult(); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.java index 257d2b9aaed..02fc522ec7e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/footer/Footer.java @@ -25,7 +25,6 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.ValueListBox; import com.google.gwt.user.client.ui.Widget; import com.google.web.bindery.event.shared.EventBus; -import com.sap.sailing.gwt.common.client.GWTLocaleUtil; import com.sap.sailing.gwt.home.desktop.app.DesktopPlacesNavigator; import com.sap.sailing.gwt.home.desktop.places.whatsnew.WhatsNewPlace; import com.sap.sailing.gwt.home.desktop.places.whatsnew.WhatsNewPlace.WhatsNewNavigationTabs; @@ -33,6 +32,7 @@ import com.sap.sailing.gwt.home.shared.SwitchingEntryPoint; import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; +import com.sap.sse.gwt.client.GWTLocaleUtil; public class Footer extends Composite { private static FooterPanelUiBinder uiBinder = GWT.create(FooterPanelUiBinder.class); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/AbstractEventActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/AbstractEventActivity.java index a964d265dbc..c2d804bc70e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/AbstractEventActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/AbstractEventActivity.java @@ -2,6 +2,7 @@ package com.sap.sailing.gwt.home.desktop.places.event; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import com.google.gwt.activity.shared.AbstractActivity; import com.google.gwt.core.shared.GWT; @@ -33,6 +34,9 @@ import com.sap.sailing.gwt.home.shared.places.event.EventDefaultPlace; import com.sap.sailing.gwt.home.shared.places.events.EventsPlace; import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesDefaultPlace; import com.sap.sailing.gwt.home.shared.places.start.StartPlace; +import com.sap.sailing.gwt.settings.client.EntryPointWithSettingsLinkFactory; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaOverviewBaseSettings; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSettings; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; @@ -229,19 +233,20 @@ public abstract class AbstractEventActivity ex @Override public String getRegattaOverviewLink() { - String url = "RegattaOverview.html?ignoreLocalSettings=true&onlyrunningraces=false&event=" + getCtx().getEventId(); - url += "&onlyracesofsameday=" + eventDTO.isRunning(); + RegattaRaceStatesSettings regattaRaceStatesSettings = new RegattaRaceStatesSettings(); + regattaRaceStatesSettings.setShowOnlyCurrentlyRunningRaces(false); + regattaRaceStatesSettings.setShowOnlyRaceOfSameDay(eventDTO.isRunning()); if(showRegattaMetadata()) { if(getRegattaMetadata().isFlexibleLeaderboard()) { String defaultCourseAreaId = getRegattaMetadata().getDefaultCourseAreaId(); if(defaultCourseAreaId != null && !defaultCourseAreaId.isEmpty()) { - url += "&coursearea=" + defaultCourseAreaId; + regattaRaceStatesSettings.getVisibleCourseAreaSettings().addValue(UUID.fromString(defaultCourseAreaId)); } } else { - url += "®atta=" + getRegattaId(); + regattaRaceStatesSettings.getVisibleRegattaSettings().addValue(getRegattaId()); } } - return url; + return EntryPointWithSettingsLinkFactory.createRegattaOverviewLink(new RegattaOverviewBaseSettings(getCtx().getEventId()), regattaRaceStatesSettings); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/TabletAndDesktopUserProfileView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/TabletAndDesktopUserProfileView.ui.xml index 0798d2ce2cc..1cc220e8092 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/TabletAndDesktopUserProfileView.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/TabletAndDesktopUserProfileView.ui.xml @@ -45,6 +45,9 @@ + + + \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileActivity.java index 094b20b1d0c..fb850c0e925 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileActivity.java @@ -17,9 +17,7 @@ import com.sap.sailing.gwt.home.shared.places.fakeseries.AbstractSeriesPlace; import com.sap.sailing.gwt.home.shared.places.start.StartPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; import com.sap.sse.security.ui.authentication.AuthenticationContextEvent; -import com.sap.sse.security.ui.authentication.AuthenticationManager; import com.sap.sse.security.ui.authentication.AuthenticationRequestEvent; -import com.sap.sse.security.ui.client.UserManagementServiceAsync; import com.sap.sse.security.ui.client.i18n.StringMessages; public class UserProfileActivity extends AbstractActivity implements UserProfileView.Presenter { @@ -97,8 +95,8 @@ public class UserProfileActivity extends AbstractActivity implements UserProfile } @Override - public AuthenticationManager getAuthenticationManager() { - return clientFactory.getAuthenticationManager(); + public UserProfileClientFactory getClientFactory() { + return clientFactory; } @Override @@ -107,8 +105,4 @@ public class UserProfileActivity extends AbstractActivity implements UserProfile .getMailVerifiedConfirmationNavigation().getTargetUrl(); } - @Override - public UserManagementServiceAsync getUserManagementService() { - return clientFactory.getUserManagementService(); - } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileClientFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileClientFactory.java index bedc9c5a2bf..79ed2b265c5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileClientFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileClientFactory.java @@ -1,8 +1,9 @@ package com.sap.sailing.gwt.home.desktop.places.user.profile; +import com.sap.sailing.gwt.home.shared.app.ClientFactoryWithDispatch; import com.sap.sailing.gwt.ui.client.SailingClientFactory; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; import com.sap.sse.security.ui.authentication.WithAuthenticationManager; -public interface UserProfileClientFactory extends SailingClientFactory, ErrorAndBusyClientFactory, WithAuthenticationManager { +public interface UserProfileClientFactory extends ClientFactoryWithDispatch, SailingClientFactory, ErrorAndBusyClientFactory, WithAuthenticationManager { } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileView.java index aa0daa52deb..86298d7e8dd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/UserProfileView.java @@ -8,10 +8,8 @@ import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.places.fakeseries.AbstractSeriesPlace; import com.sap.sailing.gwt.home.shared.places.start.StartPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; -import com.sap.sse.security.ui.authentication.AuthenticationManager; import com.sap.sse.security.ui.authentication.app.NeedsAuthenticationContext; import com.sap.sse.security.ui.authentication.decorator.NotLoggedInPresenter; -import com.sap.sse.security.ui.client.UserManagementServiceAsync; public interface UserProfileView extends IsWidget, NeedsAuthenticationContext { @@ -24,10 +22,7 @@ public interface UserProfileView getHomeNavigation(); PlaceNavigation getUserProfileNavigation(); - AuthenticationManager getAuthenticationManager(); - - UserManagementServiceAsync getUserManagementService(); - + UserProfileClientFactory getClientFactory(); String getMailVerifiedUrl(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/detailstab/UserProfileDetailsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/detailstab/UserProfileDetailsPresenter.java index 97ca4c126e7..cc5b7717c9b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/detailstab/UserProfileDetailsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/detailstab/UserProfileDetailsPresenter.java @@ -15,7 +15,8 @@ public class UserProfileDetailsPresenter implements UserProfileDetailsView.Prese this.userProfilePresenter = userProfilePresenter; view.setPresenter(this); this.userDetailsPresenter = new UserDetailsPresenter(view.getUserDetailsView(), - userProfilePresenter.getAuthenticationManager(), userProfilePresenter.getUserManagementService(), + userProfilePresenter.getClientFactory().getAuthenticationManager(), + userProfilePresenter.getClientFactory().getUserManagementService(), userProfilePresenter.getMailVerifiedUrl()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesPresenter.java new file mode 100644 index 00000000000..aae72f74651 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesPresenter.java @@ -0,0 +1,133 @@ +package com.sap.sailing.gwt.home.desktop.places.user.profile.preferencestab; + +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.communication.user.profile.CompetitorSuggestionResult; +import com.sap.sailing.gwt.home.communication.user.profile.FavoriteBoatClassesDTO; +import com.sap.sailing.gwt.home.communication.user.profile.FavoriteCompetitorsDTO; +import com.sap.sailing.gwt.home.communication.user.profile.FavoritesResult; +import com.sap.sailing.gwt.home.communication.user.profile.GetCompetitorSuggestionAction; +import com.sap.sailing.gwt.home.communication.user.profile.GetFavoritesAction; +import com.sap.sailing.gwt.home.communication.user.profile.SaveFavoriteBoatClassesAction; +import com.sap.sailing.gwt.home.communication.user.profile.SaveFavoriteCompetitorsAction; +import com.sap.sailing.gwt.home.desktop.places.user.profile.UserProfileView; +import com.sap.sailing.gwt.home.shared.partials.multiselection.AbstractSuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.AbstractSuggestedMultiSelectionCompetitorDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider; +import com.sap.sse.gwt.dispatch.shared.commands.VoidResult; +import com.sap.sse.security.ui.authentication.app.AuthenticationContext; + +public class UserProfilePreferencesPresenter implements UserProfilePreferencesView.Presenter { + + private final UserProfilePreferencesView view; + private final UserProfileView.Presenter userProfilePresenter; + private final SuggestedMultiSelectionCompetitorDataProvider competitorDataProvider = + new SuggestedMultiSelectionCompetitorDataProviderImpl(); + private final SuggestedMultiSelectionBoatClassDataProvider boatClassDataProvider = + new SuggestedMultiSelectionBoatClassDataProviderImpl(); + + public UserProfilePreferencesPresenter(final UserProfilePreferencesView view, + final UserProfileView.Presenter userProfilePresenter) { + this.view = view; + this.userProfilePresenter = userProfilePresenter; + view.setPresenter(this); + } + + @Override + public void setAuthenticationContext(AuthenticationContext authenticationContext) { + view.getDecorator().setAuthenticationContext(authenticationContext); + if (authenticationContext.isLoggedIn()) { + userProfilePresenter.getClientFactory().getDispatch().execute(new GetFavoritesAction(), + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + userProfilePresenter.getClientFactory() + .createErrorView("Error while loading notification preferences!", caught); + } + + @Override + public void onSuccess(FavoritesResult result) { + initFavoriteCompetitors(result.getFavoriteCompetitors()); + initFavoriteBoatClasses(result.getFavoriteBoatClasses()); + } + }); + } + } + + private void initFavoriteCompetitors(FavoriteCompetitorsDTO favoriteCompetitors) { + competitorDataProvider.initNotifications(favoriteCompetitors.isNotifyAboutResults()); + competitorDataProvider.initSelectedItems(favoriteCompetitors.getSelectedCompetitors()); + } + + private void initFavoriteBoatClasses(FavoriteBoatClassesDTO favoriteBoatClasses) { + boatClassDataProvider.initNotifications(favoriteBoatClasses.isNotifyAboutUpcomingRaces(), + favoriteBoatClasses.isNotifyAboutResults()); + boatClassDataProvider.initSelectedItems(favoriteBoatClasses.getSelectedBoatClasses()); + } + + @Override + public void doTriggerLoginForm() { + userProfilePresenter.doTriggerLoginForm(); + } + + @Override + public SuggestedMultiSelectionBoatClassDataProvider getFavoriteBoatClassesDataProvider() { + return boatClassDataProvider; + } + + @Override + public SuggestedMultiSelectionCompetitorDataProvider getFavoriteCompetitorsDataProvider() { + return competitorDataProvider; + } + + private class SuggestedMultiSelectionBoatClassDataProviderImpl + extends AbstractSuggestedMultiSelectionBoatClassDataProvider { + @Override + protected void persist(FavoriteBoatClassesDTO favorites) { + userProfilePresenter.getClientFactory().getDispatch().execute( + new SaveFavoriteBoatClassesAction(favorites), new SaveAsyncCallback()); + } + } + + private class SuggestedMultiSelectionCompetitorDataProviderImpl + extends AbstractSuggestedMultiSelectionCompetitorDataProvider { + + @Override + protected void getSuggestions(Iterable queryTokens, int limit, + final SuggestionItemsCallback callback) { + userProfilePresenter.getClientFactory().getDispatch().execute( + new GetCompetitorSuggestionAction(queryTokens, limit), + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + Window.alert("Error while loading competitor suggestion"); + } + + @Override + public void onSuccess(CompetitorSuggestionResult result) { + callback.setSuggestionItems(result.getValues()); + } + }); + } + + @Override + protected void persist(FavoriteCompetitorsDTO favorites) { + userProfilePresenter.getClientFactory().getDispatch().execute(new SaveFavoriteCompetitorsAction(favorites), + new SaveAsyncCallback()); + } + } + + private class SaveAsyncCallback implements AsyncCallback { + @Override + public void onFailure(Throwable caught) { + userProfilePresenter.getClientFactory().createErrorView( + "Error while saving notification preferences!", caught); + } + + @Override + public void onSuccess(VoidResult result) { + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesTabView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesTabView.java new file mode 100644 index 00000000000..1171775ba5a --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesTabView.java @@ -0,0 +1,50 @@ +package com.sap.sailing.gwt.home.desktop.places.user.profile.preferencestab; + +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.gwt.user.client.ui.Composite; +import com.sap.sailing.gwt.common.client.controls.tabbar.TabView; +import com.sap.sailing.gwt.home.desktop.places.user.profile.UserProfileTabView; +import com.sap.sailing.gwt.home.desktop.places.user.profile.UserProfileView; +import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.UserProfilePreferencesPlace; +import com.sap.sse.security.ui.authentication.app.AuthenticationContext; + +public class UserProfilePreferencesTabView extends Composite implements UserProfileTabView { + + private UserProfilePreferencesView.Presenter currentPresenter; + private UserProfilePreferencesView view; + + @Override + public Class getPlaceClassForActivation() { + return UserProfilePreferencesPlace.class; + } + + @Override + public TabView.State getState() { + return TabView.State.VISIBLE; + } + + @Override + public void start(UserProfilePreferencesPlace myPlace, AcceptsOneWidget contentArea) { + contentArea.setWidget(view); + } + + @Override + public void setAuthenticationContext(AuthenticationContext authenticationContext) { + currentPresenter.setAuthenticationContext(authenticationContext); + } + + @Override + public void stop() { + } + + @Override + public UserProfilePreferencesPlace placeToFire() { + return new UserProfilePreferencesPlace(); + } + + @Override + public void setPresenter(UserProfileView.Presenter currentPresenter) { + view = new UserProfilePreferencesViewImpl(); + this.currentPresenter = new UserProfilePreferencesPresenter(view, currentPresenter); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesView.java new file mode 100644 index 00000000000..6d14177827a --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesView.java @@ -0,0 +1,19 @@ +package com.sap.sailing.gwt.home.desktop.places.user.profile.preferencestab; + +import com.google.gwt.user.client.ui.IsWidget; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider; +import com.sap.sse.security.ui.authentication.app.NeedsAuthenticationContext; +import com.sap.sse.security.ui.authentication.decorator.NotLoggedInPresenter; + +public interface UserProfilePreferencesView extends IsWidget { + + void setPresenter(Presenter presenter); + + NeedsAuthenticationContext getDecorator(); + + public interface Presenter extends NotLoggedInPresenter, NeedsAuthenticationContext { + SuggestedMultiSelectionBoatClassDataProvider getFavoriteBoatClassesDataProvider(); + SuggestedMultiSelectionCompetitorDataProvider getFavoriteCompetitorsDataProvider(); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesViewImpl.java new file mode 100644 index 00000000000..893a7054bde --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesViewImpl.java @@ -0,0 +1,114 @@ +package com.sap.sailing.gwt.home.desktop.places.user.profile.preferencestab; + +import java.util.Collection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HasEnabled; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelection; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelection.NotificationCallback; +import com.sap.sailing.gwt.home.shared.usermanagement.decorator.AuthorizedContentDecoratorDesktop; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.security.ui.authentication.app.NeedsAuthenticationContext; + +public class UserProfilePreferencesViewImpl extends Composite implements UserProfilePreferencesView { + + interface MyBinder extends UiBinder { + } + + private static MyBinder uiBinder = GWT.create(MyBinder.class); + + @UiField(provided = true) AuthorizedContentDecoratorDesktop decoratorUi; + @UiField(provided = true) SuggestedMultiSelection favoriteCompetitorsSelctionUi; + @UiField(provided = true) SuggestedMultiSelection favoriteBoatClassesSelctionUi; + @UiField DivElement notificationsTextUi; + + @Override + public void setPresenter(Presenter presenter) { + decoratorUi = new AuthorizedContentDecoratorDesktop(presenter); + favoriteCompetitorsSelctionUi = new CompetitorDisplayImpl( + presenter.getFavoriteCompetitorsDataProvider()).selectionUi; + favoriteBoatClassesSelctionUi = new BoatClassDisplayImpl( + presenter.getFavoriteBoatClassesDataProvider()).selectionUi; + initWidget(uiBinder.createAndBindUi(this)); + // TODO hide notificationsTextUi if the user's mail address is already verified + } + + @Override + public NeedsAuthenticationContext getDecorator() { + return decoratorUi; + } + + private class CompetitorDisplayImpl implements SuggestedMultiSelectionCompetitorDataProvider.Display { + private final SuggestedMultiSelection selectionUi; + private final HasEnabled notifyAboutResultsUi; + + private CompetitorDisplayImpl(final SuggestedMultiSelectionCompetitorDataProvider dataProvider) { + selectionUi = SuggestedMultiSelection.forCompetitors(dataProvider, StringMessages.INSTANCE.favoriteCompetitors()); + notifyAboutResultsUi = selectionUi.addNotificationToggle(new NotificationCallback() { + @Override + public void onNotificationToggled(boolean enabled) { + dataProvider.setNotifyAboutResults(enabled); + } + }, StringMessages.INSTANCE.notificationAboutNewResults()); + dataProvider.addDisplay(this); + } + + @Override + public void setSelectedItems(Collection selectedItems) { + selectionUi.setSelectedItems(selectedItems); + } + + @Override + public void setNotifyAboutResults(boolean notifyAboutResults) { + notifyAboutResultsUi.setEnabled(notifyAboutResults); + } + } + + private class BoatClassDisplayImpl implements SuggestedMultiSelectionBoatClassDataProvider.Display { + private final SuggestedMultiSelection selectionUi; + private final HasEnabled notifyAboutUpcomingRacesUi; + private final HasEnabled notifyAboutResultsUi; + + private BoatClassDisplayImpl(final SuggestedMultiSelectionBoatClassDataProvider dataProvider) { + selectionUi = SuggestedMultiSelection.forBoatClasses(dataProvider, StringMessages.INSTANCE.favoriteBoatClasses()); + notifyAboutUpcomingRacesUi = selectionUi.addNotificationToggle(new NotificationCallback() { + @Override + public void onNotificationToggled(boolean enabled) { + dataProvider.setNotifyAboutUpcomingRaces(enabled); + } + }, StringMessages.INSTANCE.notificationAboutUpcomingRaces()); + notifyAboutResultsUi = selectionUi.addNotificationToggle(new NotificationCallback() { + @Override + public void onNotificationToggled(boolean enabled) { + dataProvider.setNotifyAboutResults(enabled); + } + }, StringMessages.INSTANCE.notificationAboutNewResults()); + dataProvider.addDisplay(this); + } + + @Override + public void setSelectedItems(Collection selectedItems) { + selectionUi.setSelectedItems(selectedItems); + } + + @Override + public void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces) { + notifyAboutUpcomingRacesUi.setEnabled(notifyAboutUpcomingRaces); + } + + @Override + public void setNotifyAboutResults(boolean notifyAboutResults) { + notifyAboutResultsUi.setEnabled(notifyAboutResults); + } + } + +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesViewImpl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesViewImpl.ui.xml new file mode 100644 index 00000000000..827cd59c64c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/user/profile/preferencestab/UserProfilePreferencesViewImpl.ui.xml @@ -0,0 +1,24 @@ + + + + + + + + +
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobilePlacesNavigator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobilePlacesNavigator.java index 3723e1ac40d..55965701014 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobilePlacesNavigator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobilePlacesNavigator.java @@ -28,6 +28,7 @@ import com.sap.sailing.gwt.home.shared.places.user.confirmation.ConfirmationPlac import com.sap.sailing.gwt.home.shared.places.user.passwordreset.PasswordResetPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; import com.sap.sailing.gwt.home.shared.places.user.profile.UserProfileDefaultPlace; +import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.UserProfilePreferencesPlace; public class MobilePlacesNavigator extends AbstractPlaceNavigator { @@ -105,6 +106,10 @@ public class MobilePlacesNavigator extends AbstractPlaceNavigator { public PlaceNavigation getUserProfileNavigation() { return createLocalPlaceNavigation(new UserProfileDefaultPlace()); } + + public PlaceNavigation getUserPreferencesNavigation() { + return createLocalPlaceNavigation(new UserProfilePreferencesPlace()); + } public PlaceNavigation getMailVerifiedConfirmationNavigation() { return createLocalPlaceNavigation(new ConfirmationPlace(Action.MAIL_VERIFIED)); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/AbstractUserProfileView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/AbstractUserProfileView.java new file mode 100644 index 00000000000..f3e889d86b3 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/AbstractUserProfileView.java @@ -0,0 +1,57 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.common.client.SharedResources; +import com.sap.sailing.gwt.home.mobile.partials.quickfinder.Quickfinder; +import com.sap.sailing.gwt.home.shared.usermanagement.decorator.AuthorizedContentDecoratorMobile; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.security.ui.authentication.app.AuthenticationContext; +import com.sap.sse.security.ui.userprofile.mobile.userheader.UserHeader; + +public class AbstractUserProfileView extends Composite implements UserProfileViewBase { + + private static AbstractUserProfileViewUiBinder uiBinder = GWT.create(AbstractUserProfileViewUiBinder.class); + + interface AbstractUserProfileViewUiBinder extends UiBinder { + } + + static class AbstractUserProfileViewLayout { + @UiField(provided = true) final AuthorizedContentDecoratorMobile decoratorUi; + @UiField(provided = true) final UserHeader userHeaderUi; + @UiField Quickfinder quickfinderUi; + @UiField SimplePanel contentContainerUi; + + private AbstractUserProfileViewLayout(UserProfileViewBase.Presenter presenter) { + this.decoratorUi = new AuthorizedContentDecoratorMobile(presenter); + this.userHeaderUi = new UserHeader(SharedResources.INSTANCE); + } + } + + protected final StringMessages i18n = StringMessages.INSTANCE; + private final AbstractUserProfileViewLayout layout; + + protected AbstractUserProfileView(UserProfileViewBase.Presenter presenter) { + this.layout = new AbstractUserProfileViewLayout(presenter); + initWidget(uiBinder.createAndBindUi(this.layout)); + this.layout.quickfinderUi.addPlaceholderItem(i18n.profileQuickfinder()); + this.layout.quickfinderUi.addItem(i18n.details(), presenter.getUserProfileNavigation()); + this.layout.quickfinderUi.addItem(i18n.favoritesAndNotifications(), presenter.getUserPreferencesNavigation()); + } + + protected final void setViewContent(IsWidget content) { + this.layout.contentContainerUi.setWidget(content); + } + + @Override + public void setAuthenticationContext(AuthenticationContext authenticationContext) { + this.layout.decoratorUi.setAuthenticationContext(authenticationContext); + this.layout.userHeaderUi.setAuthenticationContext(authenticationContext); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/AbstractUserProfileView.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/AbstractUserProfileView.ui.xml new file mode 100644 index 00000000000..7b4704e0597 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/AbstractUserProfileView.ui.xml @@ -0,0 +1,24 @@ + + + + + .headerWrapper { + background-color: #f2f2f2; + padding-bottom: 10px; + } + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileActivityProxy.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileActivityProxy.java index 714c480ecce..84f889d582f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileActivityProxy.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileActivityProxy.java @@ -2,7 +2,10 @@ package com.sap.sailing.gwt.home.mobile.places.user.profile; import com.google.gwt.core.client.GWT; import com.sap.sailing.gwt.home.mobile.app.MobileApplicationClientFactory; +import com.sap.sailing.gwt.home.mobile.places.user.profile.details.UserProfileDetailsActivity; +import com.sap.sailing.gwt.home.mobile.places.user.profile.preferences.UserProfilePreferencesActivity; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; +import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.UserProfilePreferencesPlace; import com.sap.sse.gwt.client.mvp.AbstractActivityProxy; public class UserProfileActivityProxy extends AbstractActivityProxy { @@ -20,7 +23,12 @@ public class UserProfileActivityProxy extends AbstractActivityProxy { GWT.runAsync(new AbstractRunAsyncCallback() { @Override public void onSuccess() { - super.onSuccess(new UserProfileDetailsActivity(currentPlace, clientFactory)); + if (currentPlace instanceof UserProfilePreferencesPlace) { + UserProfilePreferencesPlace userProfilePrefsPlace = (UserProfilePreferencesPlace) currentPlace; + super.onSuccess(new UserProfilePreferencesActivity(userProfilePrefsPlace, clientFactory)); + } else { + super.onSuccess(new UserProfileDetailsActivity(currentPlace, clientFactory)); + } } }); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsView.java deleted file mode 100644 index c7bce74050d..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsView.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sap.sailing.gwt.home.mobile.places.user.profile; - -import com.google.gwt.user.client.ui.IsWidget; -import com.sap.sse.security.ui.authentication.app.NeedsAuthenticationContext; -import com.sap.sse.security.ui.authentication.decorator.NotLoggedInPresenter; -import com.sap.sse.security.ui.userprofile.shared.userdetails.UserDetailsView; - -public interface UserProfileDetailsView extends IsWidget, NeedsAuthenticationContext { - - UserDetailsView getUserDetailsView(); - - public interface Presenter extends NotLoggedInPresenter { - } -} - diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsViewImpl.java deleted file mode 100644 index 7e8dea11d35..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsViewImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sap.sailing.gwt.home.mobile.places.user.profile; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.uibinder.client.UiBinder; -import com.google.gwt.uibinder.client.UiField; -import com.google.gwt.user.client.ui.Composite; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.common.client.SharedResources; -import com.sap.sailing.gwt.home.shared.usermanagement.decorator.AuthorizedContentDecoratorMobile; -import com.sap.sse.security.ui.authentication.app.AuthenticationContext; -import com.sap.sse.security.ui.userprofile.mobile.userdetails.UserDetails; -import com.sap.sse.security.ui.userprofile.mobile.userheader.UserHeader; -import com.sap.sse.security.ui.userprofile.shared.userdetails.UserDetailsView; - -public class UserProfileDetailsViewImpl extends Composite implements UserProfileDetailsView { - private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); - - interface MyUiBinder extends UiBinder { - } - - @UiField(provided = true) final UserHeader userHeaderUi; - @UiField(provided = true) final AuthorizedContentDecoratorMobile decoratorUi; - @UiField(provided = true) final UserDetails userDetailsUi; - - public UserProfileDetailsViewImpl(Presenter presenter) { - userHeaderUi = new UserHeader(SharedResources.INSTANCE); - decoratorUi = new AuthorizedContentDecoratorMobile(presenter); - userDetailsUi = new UserDetails(SharedResources.INSTANCE); - initWidget(uiBinder.createAndBindUi(this)); - } - - @Override - public UserDetailsView getUserDetailsView() { - return userDetailsUi; - } - - @Override - public void setAuthenticationContext(AuthenticationContext authenticationContext) { - userHeaderUi.setAuthenticationContext(authenticationContext); - decoratorUi.setAuthenticationContext(authenticationContext); - } -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsViewImpl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsViewImpl.ui.xml deleted file mode 100644 index e6d16a49d82..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsViewImpl.ui.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileViewBase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileViewBase.java new file mode 100644 index 00000000000..67fdac7ff66 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileViewBase.java @@ -0,0 +1,15 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile; + +import com.google.gwt.user.client.ui.IsWidget; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; +import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; +import com.sap.sse.security.ui.authentication.app.NeedsAuthenticationContext; +import com.sap.sse.security.ui.authentication.decorator.NotLoggedInPresenter; + +public interface UserProfileViewBase extends IsWidget, NeedsAuthenticationContext { + + public interface Presenter extends NotLoggedInPresenter { + PlaceNavigation getUserProfileNavigation(); + PlaceNavigation getUserPreferencesNavigation(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsActivity.java similarity index 75% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsActivity.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsActivity.java index c723488bbb0..44fe0b4c229 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/UserProfileDetailsActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsActivity.java @@ -1,9 +1,10 @@ -package com.sap.sailing.gwt.home.mobile.places.user.profile; +package com.sap.sailing.gwt.home.mobile.places.user.profile.details; import com.google.gwt.activity.shared.AbstractActivity; import com.google.gwt.event.shared.EventBus; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.sap.sailing.gwt.home.mobile.app.MobileApplicationClientFactory; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; import com.sap.sse.security.ui.authentication.AuthenticationContextEvent; import com.sap.sse.security.ui.userprofile.shared.userdetails.UserDetailsPresenter; @@ -12,12 +13,13 @@ public class UserProfileDetailsActivity extends AbstractActivity implements User private final MobileApplicationClientFactory clientFactory; - private final UserProfileDetailsView currentView = new UserProfileDetailsViewImpl(this); + private final UserProfileDetailsView currentView; private UserDetailsPresenter userDetailsPresenter; public UserProfileDetailsActivity(AbstractUserProfilePlace place, MobileApplicationClientFactory clientFactory) { this.clientFactory = clientFactory; + this.currentView = new UserProfileDetailsViewImpl(this); } @Override @@ -42,4 +44,14 @@ public class UserProfileDetailsActivity extends AbstractActivity implements User public void doTriggerLoginForm() { clientFactory.getNavigator().getSignInNavigation().goToPlace(); } + + @Override + public PlaceNavigation getUserProfileNavigation() { + return clientFactory.getNavigator().getUserProfileNavigation(); + } + + @Override + public PlaceNavigation getUserPreferencesNavigation() { + return clientFactory.getNavigator().getUserPreferencesNavigation(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsView.java new file mode 100644 index 00000000000..2379450979d --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsView.java @@ -0,0 +1,13 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile.details; + +import com.sap.sailing.gwt.home.mobile.places.user.profile.UserProfileViewBase; +import com.sap.sse.security.ui.userprofile.shared.userdetails.UserDetailsView; + +public interface UserProfileDetailsView extends UserProfileViewBase { + + UserDetailsView getUserDetailsView(); + + public interface Presenter extends UserProfileViewBase.Presenter { + } +} + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsViewImpl.java new file mode 100644 index 00000000000..a618bfa0aa7 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/details/UserProfileDetailsViewImpl.java @@ -0,0 +1,22 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile.details; + +import com.sap.sailing.gwt.common.client.SharedResources; +import com.sap.sailing.gwt.home.mobile.places.user.profile.AbstractUserProfileView; +import com.sap.sse.security.ui.userprofile.mobile.userdetails.UserDetails; +import com.sap.sse.security.ui.userprofile.shared.userdetails.UserDetailsView; + +public class UserProfileDetailsViewImpl extends AbstractUserProfileView implements UserProfileDetailsView { + + private final UserDetails userDetailsUi = new UserDetails(SharedResources.INSTANCE); + + public UserProfileDetailsViewImpl(UserProfileDetailsView.Presenter presenter) { + super(presenter); + setViewContent(userDetailsUi); + } + + @Override + public UserDetailsView getUserDetailsView() { + return userDetailsUi; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesActivity.java new file mode 100644 index 00000000000..9a05c5da4c5 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesActivity.java @@ -0,0 +1,147 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile.preferences; + +import com.google.gwt.activity.shared.AbstractActivity; +import com.google.gwt.event.shared.EventBus; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.communication.user.profile.CompetitorSuggestionResult; +import com.sap.sailing.gwt.home.communication.user.profile.FavoriteBoatClassesDTO; +import com.sap.sailing.gwt.home.communication.user.profile.FavoriteCompetitorsDTO; +import com.sap.sailing.gwt.home.communication.user.profile.FavoritesResult; +import com.sap.sailing.gwt.home.communication.user.profile.GetCompetitorSuggestionAction; +import com.sap.sailing.gwt.home.communication.user.profile.GetFavoritesAction; +import com.sap.sailing.gwt.home.communication.user.profile.SaveFavoriteBoatClassesAction; +import com.sap.sailing.gwt.home.communication.user.profile.SaveFavoriteCompetitorsAction; +import com.sap.sailing.gwt.home.mobile.app.MobileApplicationClientFactory; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; +import com.sap.sailing.gwt.home.shared.partials.multiselection.AbstractSuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.AbstractSuggestedMultiSelectionCompetitorDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider; +import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; +import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.UserProfilePreferencesPlace; +import com.sap.sse.gwt.dispatch.shared.commands.VoidResult; +import com.sap.sse.security.ui.authentication.AuthenticationContextEvent; + +public class UserProfilePreferencesActivity extends AbstractActivity implements UserProfilePreferencesView.Presenter { + + private final MobileApplicationClientFactory clientFactory; + private final UserProfilePreferencesView currentView;; + private final SuggestedMultiSelectionCompetitorDataProvider competitorDataProvider = + new SuggestedMultiSelectionCompetitorDataProviderImpl(); + private final SuggestedMultiSelectionBoatClassDataProvider boatClassDataProvider = + new SuggestedMultiSelectionBoatClassDataProviderImpl(); + + public UserProfilePreferencesActivity(UserProfilePreferencesPlace place, + MobileApplicationClientFactory clientFactory) { + this.clientFactory = clientFactory; + this.currentView = new UserProfilePreferencesViewImpl(this); + } + + @Override + public void start(final AcceptsOneWidget panel, final EventBus eventBus) { + panel.setWidget(currentView); + currentView.setAuthenticationContext(clientFactory.getAuthenticationManager().getAuthenticationContext()); + eventBus.addHandler(AuthenticationContextEvent.TYPE, new AuthenticationContextEvent.Handler() { + @Override + public void onUserChangeEvent(AuthenticationContextEvent event) { + currentView.setAuthenticationContext(event.getCtx()); + } + }); + clientFactory.getDispatch().execute(new GetFavoritesAction(), new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + clientFactory.createErrorView("Error while loading notification preferences!", caught); + } + + @Override + public void onSuccess(FavoritesResult result) { + initFavoriteCompetitors(result.getFavoriteCompetitors()); + initFavoriteBoatClasses(result.getFavoriteBoatClasses()); + } + }); + } + + private void initFavoriteCompetitors(FavoriteCompetitorsDTO favoriteCompetitors) { + competitorDataProvider.initNotifications(favoriteCompetitors.isNotifyAboutResults()); + competitorDataProvider.initSelectedItems(favoriteCompetitors.getSelectedCompetitors()); + } + + private void initFavoriteBoatClasses(FavoriteBoatClassesDTO favoriteBoatClasses) { + boatClassDataProvider.initNotifications(favoriteBoatClasses.isNotifyAboutUpcomingRaces(), + favoriteBoatClasses.isNotifyAboutResults()); + boatClassDataProvider.initSelectedItems(favoriteBoatClasses.getSelectedBoatClasses()); + } + + @Override + public void doTriggerLoginForm() { + clientFactory.getNavigator().getSignInNavigation().goToPlace(); + } + + @Override + public SuggestedMultiSelectionBoatClassDataProvider getFavoriteBoatClassesDataProvider() { + return boatClassDataProvider; + } + + @Override + public SuggestedMultiSelectionCompetitorDataProvider getFavoriteCompetitorsDataProvider() { + return competitorDataProvider; + } + + private class SuggestedMultiSelectionBoatClassDataProviderImpl + extends AbstractSuggestedMultiSelectionBoatClassDataProvider { + @Override + protected void persist(FavoriteBoatClassesDTO favorites) { + clientFactory.getDispatch().execute(new SaveFavoriteBoatClassesAction(favorites), new SaveAsyncCallback()); + } + } + + private class SuggestedMultiSelectionCompetitorDataProviderImpl + extends AbstractSuggestedMultiSelectionCompetitorDataProvider { + + @Override + protected void getSuggestions(Iterable queryTokens, int limit, + final SuggestionItemsCallback callback) { + clientFactory.getDispatch().execute(new GetCompetitorSuggestionAction(queryTokens, limit), + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + Window.alert("Error while loading competitor suggestion"); + } + + @Override + public void onSuccess(CompetitorSuggestionResult result) { + callback.setSuggestionItems(result.getValues()); + } + }); + } + + @Override + protected void persist(FavoriteCompetitorsDTO favorites) { + clientFactory.getDispatch().execute(new SaveFavoriteCompetitorsAction(favorites), new SaveAsyncCallback()); + } + } + + private class SaveAsyncCallback implements AsyncCallback { + @Override + public void onFailure(Throwable caught) { + clientFactory.createErrorView("Error while saving notification preferences!", caught); + } + + @Override + public void onSuccess(VoidResult result) { + } + } + + @Override + public PlaceNavigation getUserProfileNavigation() { + return clientFactory.getNavigator().getUserProfileNavigation(); + } + + @Override + public PlaceNavigation getUserPreferencesNavigation() { + return clientFactory.getNavigator().getUserPreferencesNavigation(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesView.java new file mode 100644 index 00000000000..57e5a35639a --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesView.java @@ -0,0 +1,14 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile.preferences; + +import com.sap.sailing.gwt.home.mobile.places.user.profile.UserProfileViewBase; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider; + +public interface UserProfilePreferencesView extends UserProfileViewBase { + + public interface Presenter extends UserProfileViewBase.Presenter { + SuggestedMultiSelectionBoatClassDataProvider getFavoriteBoatClassesDataProvider(); + SuggestedMultiSelectionCompetitorDataProvider getFavoriteCompetitorsDataProvider(); + } +} + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesViewImpl.java new file mode 100644 index 00000000000..befafbbce41 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesViewImpl.java @@ -0,0 +1,101 @@ +package com.sap.sailing.gwt.home.mobile.places.user.profile.preferences; + +import java.util.Collection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.HasEnabled; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.mobile.places.user.profile.AbstractUserProfileView; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelection; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelection.NotificationCallback; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider; + +public class UserProfilePreferencesViewImpl extends AbstractUserProfileView implements UserProfilePreferencesView { + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + interface MyUiBinder extends UiBinder { + } + + @UiField(provided = true) SuggestedMultiSelection favoriteCompetitorsSelctionUi; + @UiField(provided = true) SuggestedMultiSelection favoriteBoatClassesSelctionUi; + @UiField DivElement notificationsTextUi; + + public UserProfilePreferencesViewImpl(UserProfilePreferencesView.Presenter presenter) { + super(presenter); + favoriteCompetitorsSelctionUi = new CompetitorDisplayImpl( + presenter.getFavoriteCompetitorsDataProvider()).selectionUi; + favoriteBoatClassesSelctionUi = new BoatClassDisplayImpl( + presenter.getFavoriteBoatClassesDataProvider()).selectionUi; + setViewContent(uiBinder.createAndBindUi(this)); + } + + private class CompetitorDisplayImpl implements SuggestedMultiSelectionCompetitorDataProvider.Display { + private final SuggestedMultiSelection selectionUi; + private final HasEnabled notifyAboutResultsUi; + + private CompetitorDisplayImpl(final SuggestedMultiSelectionCompetitorDataProvider dataProvider) { + selectionUi = SuggestedMultiSelection.forCompetitors(dataProvider, i18n.favoriteCompetitors()); + notifyAboutResultsUi = selectionUi.addNotificationToggle(new NotificationCallback() { + @Override + public void onNotificationToggled(boolean enabled) { + dataProvider.setNotifyAboutResults(enabled); + } + }, i18n.notificationAboutNewResults()); + dataProvider.addDisplay(this); + } + + @Override + public void setSelectedItems(Collection selectedItems) { + selectionUi.setSelectedItems(selectedItems); + } + + @Override + public void setNotifyAboutResults(boolean notifyAboutResults) { + notifyAboutResultsUi.setEnabled(notifyAboutResults); + } + } + + private class BoatClassDisplayImpl implements SuggestedMultiSelectionBoatClassDataProvider.Display { + private final SuggestedMultiSelection selectionUi; + private final HasEnabled notifyAboutUpcomingRacesUi; + private final HasEnabled notifyAboutResultsUi; + + private BoatClassDisplayImpl(final SuggestedMultiSelectionBoatClassDataProvider dataProvider) { + selectionUi = SuggestedMultiSelection.forBoatClasses(dataProvider, i18n.favoriteBoatClasses()); + notifyAboutUpcomingRacesUi = selectionUi.addNotificationToggle(new NotificationCallback() { + @Override + public void onNotificationToggled(boolean enabled) { + dataProvider.setNotifyAboutUpcomingRaces(enabled); + } + }, i18n.notificationAboutUpcomingRaces()); + notifyAboutResultsUi = selectionUi.addNotificationToggle(new NotificationCallback() { + @Override + public void onNotificationToggled(boolean enabled) { + dataProvider.setNotifyAboutResults(enabled); + } + }, i18n.notificationAboutNewResults()); + dataProvider.addDisplay(this); + } + + @Override + public void setSelectedItems(Collection selectedItems) { + selectionUi.setSelectedItems(selectedItems); + } + + @Override + public void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces) { + notifyAboutUpcomingRacesUi.setEnabled(notifyAboutUpcomingRaces); + } + + @Override + public void setNotifyAboutResults(boolean notifyAboutResults) { + notifyAboutResultsUi.setEnabled(notifyAboutResults); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesViewImpl.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesViewImpl.ui.xml new file mode 100644 index 00000000000..e6547d811c1 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/user/profile/preferences/UserProfilePreferencesViewImpl.ui.xml @@ -0,0 +1,25 @@ + + + + + + .edgeToEdge { + border-radius: 0; + border-left: none; + border-right: none; + } + + +
+ +
+
+ +
+
+ +
+
+
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java index 2473ee39832..9ca7f1f0d22 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/SailingDispatchContextImpl.java @@ -12,6 +12,9 @@ import com.sap.sailing.gwt.server.HomeServiceUtil; import com.sap.sailing.news.EventNewsService; import com.sap.sailing.server.RacingEventService; import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException; +import com.sap.sse.security.SecurityService; +import com.sap.sse.security.User; +import com.sap.sse.security.UserStore; @GwtIncompatible public class SailingDispatchContextImpl implements SailingDispatchContext { @@ -21,11 +24,17 @@ public class SailingDispatchContextImpl implements SailingDispatchContext { // private final Date currentServerTime = new Date(); private String clientLocaleName; private final HttpServletRequest request; + private final SecurityService securityService; + private final UserStore userStore; - public SailingDispatchContextImpl(Date currentClientTime, RacingEventService racingEventService, EventNewsService eventNewsService, String clientLocaleName, HttpServletRequest request) { + public SailingDispatchContextImpl(Date currentClientTime, RacingEventService racingEventService, + EventNewsService eventNewsService, SecurityService securityService, UserStore userStore, + String clientLocaleName, HttpServletRequest request) { this.currentClientTime = currentClientTime; this.racingEventService = racingEventService; this.eventNewsService = eventNewsService; + this.securityService = securityService; + this.userStore = userStore; this.clientLocaleName = clientLocaleName; this.request = request; } @@ -68,4 +77,20 @@ public class SailingDispatchContextImpl implements SailingDispatchContext { return HomeServiceUtil.getRequestBaseURL(request); } + @Override + public T getPreferenceForCurrentUser(String preferenceKey) { + User currentUser = securityService.getCurrentUser(); + if (currentUser != null) { + return userStore.getPreferenceObject(currentUser.getName(), preferenceKey); + } + return null; + } + + @Override + public void setPreferenceForCurrentUser(String preferenceKey, Object preference) { + User currentUser = securityService.getCurrentUser(); + if (currentUser != null) { + userStore.setPreferenceObject(currentUser.getName(), preferenceKey, preference); + } + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java index 1186ed03aeb..b9618268f34 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/servlets/SailingDispatchServlet.java @@ -12,6 +12,8 @@ import com.sap.sse.gwt.dispatch.client.transport.gwtrpc.RequestWrapper; import com.sap.sse.gwt.dispatch.servlets.AbstractDispatchServlet; import com.sap.sse.gwt.dispatch.shared.commands.Action; import com.sap.sse.gwt.dispatch.shared.commands.Result; +import com.sap.sse.security.SecurityService; +import com.sap.sse.security.UserStore; import com.sap.sse.util.ServiceTrackerFactory; public class SailingDispatchServlet extends AbstractDispatchServlet { @@ -19,18 +21,22 @@ public class SailingDispatchServlet extends AbstractDispatchServlet racingEventServiceTracker; private final ServiceTracker eventNewsServiceTracker; + private final ServiceTracker securityServiceTracker; + private final ServiceTracker userStoreTracker; public SailingDispatchServlet() { final BundleContext context = Activator.getDefault(); racingEventServiceTracker = ServiceTrackerFactory.createAndOpen(context, RacingEventService.class); eventNewsServiceTracker = ServiceTrackerFactory.createAndOpen(context, EventNewsService.class); + securityServiceTracker = ServiceTrackerFactory.createAndOpen(context, SecurityService.class); + userStoreTracker = ServiceTrackerFactory.createAndOpen(context, UserStore.class); } @Override protected > SailingDispatchContext createDispatchContextFor( RequestWrapper request) { - return new SailingDispatchContextImpl(request.getCurrentClientTime(), - racingEventServiceTracker.getService(), eventNewsServiceTracker.getService(), request - .getClientLocaleName(), getThreadLocalRequest()); + return new SailingDispatchContextImpl(request.getCurrentClientTime(), racingEventServiceTracker.getService(), + eventNewsServiceTracker.getService(), securityServiceTracker.getService(), + userStoreTracker.getService(), request.getClientLocaleName(), getThreadLocalRequest()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/app/ApplicationHistoryMapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/app/ApplicationHistoryMapper.java index cfb4f3f68a1..ad5dc5087f2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/app/ApplicationHistoryMapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/app/ApplicationHistoryMapper.java @@ -37,6 +37,7 @@ import com.sap.sailing.gwt.home.shared.places.user.confirmation.ConfirmationPlac import com.sap.sailing.gwt.home.shared.places.user.passwordreset.PasswordResetPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.UserProfileDefaultPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.UserProfileDetailsPlace; +import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.UserProfilePreferencesPlace; @WithTokenizers({ AboutUsPlace.Tokenizer.class, ContactPlace.Tokenizer.class, EventPlace.Tokenizer.class, EventsPlace.Tokenizer.class, SolutionsPlace.Tokenizer.class, WhatsNewPlace.Tokenizer.class, @@ -70,6 +71,7 @@ import com.sap.sailing.gwt.home.shared.places.user.profile.UserProfileDetailsPla // User management places: UserProfileDefaultPlace.Tokenizer.class, UserProfileDetailsPlace.Tokenizer.class, + UserProfilePreferencesPlace.Tokenizer.class, AuthenticationPlace.Tokenizer.class, ConfirmationPlace.Tokenizer.class, PasswordResetPlace.Tokenizer.class, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractAsyncSuggestBoxFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractAsyncSuggestBoxFilter.java new file mode 100644 index 00000000000..9c60703beb7 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractAsyncSuggestBoxFilter.java @@ -0,0 +1,31 @@ +package com.sap.sailing.gwt.home.shared.partials.filter; + +import java.util.Collection; + +import com.sap.sse.common.filter.Filter; + +public abstract class AbstractAsyncSuggestBoxFilter extends AbstractSuggestBoxFilter { + + protected AbstractAsyncSuggestBoxFilter(String placeholderText) { + super(placeholderText); + } + + @Override + public final void setSelectableValues(Collection selectableValues) { + // Nothing to do here, because of external filtering + } + + @Override + protected final Iterable getMatchingStrings(C value) { + return null; // Not required here, because of external filtering + } + + @Override + protected final Filter getFilter(String searchString) { + return new Filter() { // Return an always matching filter, because of external filtering + @Override public boolean matches(T object) { return true; } + @Override public String getName() { return "alwaysMatchingFilter"; } + }; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractListSuggestBoxFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractListSuggestBoxFilter.java new file mode 100644 index 00000000000..05a3e986e0c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractListSuggestBoxFilter.java @@ -0,0 +1,37 @@ +package com.sap.sailing.gwt.home.shared.partials.filter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.google.gwt.user.client.ui.SuggestOracle.Callback; +import com.google.gwt.user.client.ui.SuggestOracle.Request; +import com.sap.sse.common.filter.AbstractListFilter; + +public abstract class AbstractListSuggestBoxFilter extends AbstractSuggestBoxFilter { + + private final List suggestionObjectList = new ArrayList<>(); + protected final AbstractListFilter suggestionMatchingFilter = new AbstractListFilter() { + @Override + public Iterable getStrings(C value) { + return getMatchingStrings(value); + } + }; + + protected AbstractListSuggestBoxFilter(String placeholderText) { + super(placeholderText); + } + + @Override + protected void getSuggestions(Request request, Callback callback, Iterable queryTokens) { + Iterable filteredList = suggestionMatchingFilter.applyFilter(queryTokens, suggestionObjectList); + this.setSuggestions(request, callback, filteredList, queryTokens); + } + + @Override + public void setSelectableValues(Collection selectableValues) { + suggestionObjectList.clear(); + suggestionObjectList.addAll(selectableValues); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractSuggestBoxFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractSuggestBoxFilter.java index 3cc965244b7..b7a0748ee39 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractSuggestBoxFilter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractSuggestBoxFilter.java @@ -14,21 +14,16 @@ import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay; import com.google.gwt.user.client.ui.SuggestBox.SuggestionCallback; import com.google.gwt.user.client.ui.SuggestOracle; +import com.google.gwt.user.client.ui.SuggestOracle.Callback; +import com.google.gwt.user.client.ui.SuggestOracle.Request; +import com.google.gwt.user.client.ui.SuggestOracle.Response; import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; import com.google.gwt.user.client.ui.TextBox; import com.sap.sse.common.Util; -import com.sap.sse.common.filter.AbstractListFilter; public abstract class AbstractSuggestBoxFilter extends AbstractTextInputFilter { private final ContainsSuggestOracle suggestOracle = new ContainsSuggestOracle(); - private final List suggestionObjectList = new ArrayList<>(); - protected final AbstractListFilter suggestionMatchingFilter = new AbstractListFilter() { - @Override - public Iterable getStrings(C value) { - return getMatchingStrings(value); - } - }; protected AbstractSuggestBoxFilter(String placeholderText) { final SuggestBox suggestBox = new SuggestBox(suggestOracle, new TextBox(), new CustomSuggestionDisplay()); @@ -36,17 +31,37 @@ public abstract class AbstractSuggestBoxFilter extends AbstractTextInputFi suggestBox.addSelectionHandler(new SelectionHandler() { @Override public void onSelection(SelectionEvent event) { + @SuppressWarnings("unchecked") + C suggestObject = ((SimpleSuggestion) event.getSelectedItem()).suggestObject; + AbstractSuggestBoxFilter.this.onSuggestionSelected(suggestObject); AbstractSuggestBoxFilter.super.update(); } }); } - @Override - public void setSelectableValues(Collection selectableValues) { - suggestionObjectList.clear(); - suggestionObjectList.addAll(selectableValues); + protected final void setSuggestions(Request request, Callback callback, Iterable suggestionObjects, + Iterable queryTokens) { + List suggestions = new ArrayList<>(); + List normalizedQueryTokens = new ArrayList<>(); + for (String token : queryTokens) { + normalizedQueryTokens.add(token.toLowerCase()); + } + int count = 0; + for (C match : suggestionObjects) { + suggestions.add(new SimpleSuggestion(match, normalizedQueryTokens)); + if (++count >= request.getLimit()) { + break; + } + } + Response response = new Response(suggestions); + response.setMoreSuggestionsCount(Util.size(suggestionObjects) - count); + callback.onSuggestionsReady(request, response); } + protected abstract void getSuggestions(Request request, Callback callback, Iterable queryTokens); + + protected abstract void onSuggestionSelected(C selectedItem); + protected abstract String createSuggestionKeyString(C value); protected abstract String createSuggestionAdditionalDisplayString(C value); @@ -66,7 +81,7 @@ public abstract class AbstractSuggestBoxFilter extends AbstractTextInputFi @Override public void requestDefaultSuggestions(Request request, Callback callback) { - setSuggestions(request, callback, suggestionObjectList, Collections.emptyList()); + getSuggestions(request, callback, Collections.emptyList()); } @Override @@ -76,32 +91,13 @@ public abstract class AbstractSuggestBoxFilter extends AbstractTextInputFi requestDefaultSuggestions(request, callback); } else { Iterable queryTokens = Collections.singleton(normalizedQuery); - Iterable filteredList = suggestionMatchingFilter.applyFilter(queryTokens, suggestionObjectList); - setSuggestions(request, callback, filteredList, queryTokens); + getSuggestions(request, callback, queryTokens); } } - private void setSuggestions(Request request, Callback callback, Iterable suggestionObjects, Iterable queryTokens) { - List suggestions = new ArrayList<>(); - List normalizedQueryTokens = new ArrayList<>(); - for (String token : queryTokens) { - normalizedQueryTokens.add(token.toLowerCase()); - } - int count = 0; - for (C match : suggestionObjects) { - suggestions.add(new SimpleSuggestion(match, normalizedQueryTokens)); - if (++count >= request.getLimit()) { - break; - } - } - Response response = new Response(suggestions); - response.setMoreSuggestionsCount(Util.size(suggestionObjects) - count); - callback.onSuggestionsReady(request, response); - } - @Override public boolean isDisplayStringHTML() { - return true; + return true; } } @@ -121,10 +117,13 @@ public abstract class AbstractSuggestBoxFilter extends AbstractTextInputFi public String getDisplayString() { SafeHtmlBuilder builder = new SafeHtmlBuilder(); appendHighlighted(builder, createSuggestionKeyString(suggestObject)); - builder.append(SafeHtmlUtils.fromTrustedString(ITALIC_TAG_OPEN)); - builder.append(SafeHtmlUtils.fromSafeConstant(" - ")); - appendHighlighted(builder, createSuggestionAdditionalDisplayString(suggestObject)); - builder.append(SafeHtmlUtils.fromTrustedString(ITALIC_TAG_CLOSE)); + String additionalString = createSuggestionAdditionalDisplayString(suggestObject); + if(additionalString != null && !additionalString.isEmpty()) { + builder.append(SafeHtmlUtils.fromTrustedString(ITALIC_TAG_OPEN)); + builder.append(SafeHtmlUtils.fromSafeConstant(" - ")); + appendHighlighted(builder, additionalString); + builder.append(SafeHtmlUtils.fromTrustedString(ITALIC_TAG_CLOSE)); + } return builder.toSafeHtml().asString(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractTextInputFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractTextInputFilter.java index 0eb71be8c0e..7f1216c5e74 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractTextInputFilter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/AbstractTextInputFilter.java @@ -58,17 +58,21 @@ public abstract class AbstractTextInputFilter extends AbstractFilterWidget notifyValueChangeHandlers(); } + protected void clear() { + textInputUi.setValue(null); + update(); + } + protected abstract Filter getFilter(String searchString); private class TextInputFilterHandler implements ClickHandler, KeyUpHandler { @Override public void onKeyUp(KeyUpEvent event) { - update(); + AbstractTextInputFilter.this.update(); } @Override public void onClick(ClickEvent event) { - textInputUi.setValue(null); - update(); + AbstractTextInputFilter.this.clear(); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/RacesByCompetitorTextBoxFilter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/RacesByCompetitorTextBoxFilter.java index dfdbdcc98e5..fae3a0e0c02 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/RacesByCompetitorTextBoxFilter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/filter/RacesByCompetitorTextBoxFilter.java @@ -10,7 +10,7 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.Util; import com.sap.sse.common.filter.Filter; -public class RacesByCompetitorTextBoxFilter extends AbstractSuggestBoxFilter { +public class RacesByCompetitorTextBoxFilter extends AbstractListSuggestBoxFilter { private final RacesByCompetitorFilter filter = new RacesByCompetitorFilter(); @@ -27,6 +27,10 @@ public class RacesByCompetitorTextBoxFilter extends AbstractSuggestBoxFilter + implements SuggestedMultiSelectionBoatClassDataProvider { + + private boolean notifyAboutUpcomingRaces; + private boolean notifyAboutResults; + + private final AbstractListFilter filter = new AbstractListFilter() { + @Override + public Iterable getStrings(BoatClassMasterdata item) { + return item.getBoatClassNames(); + } + }; + + protected AbstractSuggestedMultiSelectionBoatClassDataProvider() { + super(new ProvidesKey() { + @Override + public Object getKey(BoatClassDTO item) { + return item.getName(); + } + }); + } + + @Override + protected void getSuggestions(Iterable queryTokens, int limit, + SuggestionItemsCallback callback) { + List boatClasses = Arrays.asList(BoatClassMasterdata.values()); + List suggestionItems = new ArrayList<>(); + for (BoatClassMasterdata bcm : filter.applyFilter(queryTokens, boatClasses)) { + suggestionItems.add(new BoatClassDTO(bcm.getDisplayName(), bcm.getHullLength().getMeters())); + } + callback.setSuggestionItems(suggestionItems); + } + + @Override + public void initNotifications(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults) { + this.notifyAboutUpcomingRaces = notifyAboutUpcomingRaces; + this.notifyAboutResults = notifyAboutResults; + for (SuggestedMultiSelectionBoatClassDataProvider.Display display : displays) { + display.setNotifyAboutUpcomingRaces(notifyAboutUpcomingRaces); + display.setNotifyAboutResults(notifyAboutResults); + } + } + + @Override + public void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces) { + this.notifyAboutUpcomingRaces = notifyAboutUpcomingRaces; + this.persist(); + } + + @Override + public void setNotifyAboutResults(boolean notifyAboutResults) { + this.notifyAboutResults = notifyAboutResults; + this.persist(); + } + + @Override + protected final void persist(Collection selectedItem) { + this.persist(new FavoriteBoatClassesDTO(selectedItem, notifyAboutUpcomingRaces, notifyAboutResults)); + } + + protected abstract void persist(FavoriteBoatClassesDTO favorites); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionCompetitorDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionCompetitorDataProvider.java new file mode 100644 index 00000000000..75419bf1113 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionCompetitorDataProvider.java @@ -0,0 +1,45 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import java.util.Collection; + +import com.google.gwt.view.client.ProvidesKey; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.communication.user.profile.FavoriteCompetitorsDTO; + +public abstract class AbstractSuggestedMultiSelectionCompetitorDataProvider extends + AbstractSuggestedMultiSelectionDataProvider + implements SuggestedMultiSelectionCompetitorDataProvider { + + private boolean notifyAboutResults; + + protected AbstractSuggestedMultiSelectionCompetitorDataProvider() { + super(new ProvidesKey() { + @Override + public Object getKey(SimpleCompetitorWithIdDTO item) { + return item.getIdAsString(); + } + }); + } + + @Override + public void initNotifications(boolean notifyAboutResults) { + this.notifyAboutResults = notifyAboutResults; + for (SuggestedMultiSelectionCompetitorDataProvider.Display display : displays) { + display.setNotifyAboutResults(notifyAboutResults); + } + } + + @Override + public void setNotifyAboutResults(boolean notifyAboutResults) { + this.notifyAboutResults = notifyAboutResults; + this.persist(); + } + + @Override + protected final void persist(Collection selectedItem) { + this.persist(new FavoriteCompetitorsDTO(selectedItem, notifyAboutResults)); + } + + protected abstract void persist(FavoriteCompetitorsDTO favorites); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionDataProvider.java new file mode 100644 index 00000000000..22ab4f5a5c2 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/AbstractSuggestedMultiSelectionDataProvider.java @@ -0,0 +1,95 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.view.client.ProvidesKey; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionDataProvider.Display; + +public abstract class AbstractSuggestedMultiSelectionDataProvider> + implements SuggestedMultiSelectionDataProvider { + + private final ProvidesKey keyProvider; + private final Map selectedItemsMap = new HashMap<>(); + protected Set displays = new HashSet<>(); + + protected AbstractSuggestedMultiSelectionDataProvider(ProvidesKey keyProvider) { + this.keyProvider = keyProvider; + } + + @Override + public final void addDisplay(D display) { + this.displays.add(display); + } + + @Override + public final Object getKey(T item) { + return keyProvider == null ? item : keyProvider.getKey(item); + } + + @Override + public final void addSelection(T item) { + Object key = getKey(item); + selectedItemsMap.put(key, item); + persist(); + } + + @Override + public final void removeSelection(T item) { + Object key = getKey(item); + selectedItemsMap.remove(key); + persist(); + } + + @Override + public final void clearSelection() { + selectedItemsMap.clear(); + persist(); + } + + @Override + public final void initSelectedItems(Collection selectedItems) { + selectedItemsMap.clear(); + for (T item : selectedItems) { + selectedItemsMap.put(getKey(item), item); + } + for (D display : displays) { + display.setSelectedItems(selectedItems); + } + } + + @Override + public final void getSuggestionItems(Iterable queryTokens, int limit, + final SuggestionItemsCallback callback) { + final SuggestionItemsCallback internalCallback = new SuggestionItemsCallback() { + @Override + public void setSuggestionItems(Collection suggestionItems) { + List filteredSuggestionItems = new ArrayList<>(); + for (T item : suggestionItems) { + Object key = getKey(item); + if (!selectedItemsMap.containsKey(key)) { + filteredSuggestionItems.add(item); + } + } + callback.setSuggestionItems(filteredSuggestionItems); + } + }; + this.getSuggestions(queryTokens, limit, internalCallback); + } + + @Override + public final void persist() { + this.persist(new ArrayList<>(selectedItemsMap.values())); + } + + protected abstract void getSuggestions(Iterable queryTokens, int limit, + SuggestionItemsCallback callback); + + protected abstract void persist(Collection selectedItem); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.gss new file mode 100644 index 00000000000..28e44565f8e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.gss @@ -0,0 +1,119 @@ +.suggestions, .suggestionsMobile { + width: 100%; + font-size: 1rem; + overflow: hidden; +} +.suggestions { + border: 1px solid #dedede; + border-radius: 0.333333333333333em; +} + +.suggestionsMobile { + border-top: 1px solid #dedede; + border-bottom: 1px solid #dedede; +} + +.suggestionsHeader { + display: block; + background-color: #f2f2f2; + color: #333; + border-bottom: 1px solid #dedede; + text-decoration: none; + position: relative; + overflow: hidden; +} + +.suggestionsHeaderTitle { + padding: 1.111111111111111em; + padding-right: 0.25em; + font-size: 1.2em; + font-weight: 700; + line-height: 1.333; + display: inline-block; +} + +.suggestionsContent { + font-size: inherit; + overflow: hidden; +} + +.suggestionsSettingsMenuItem, .suggestionsContentToolbar, .suggestionsSelectedItem { + padding: 0.333333333333333em; + position: relative; + text-align: right; +} + +.suggestionsAddFilter, .suggestionsItemDescription { + position: absolute !important; + left: 0.333333333333333em; + right: 8.666666666666667em; +} + +.suggestionsSettingsMenuItemLabel { + padding-right: 8.666666666666667em +} + +.suggestionsRemoveButton ,.suggestionsToggleButton { + padding: 0.433333333333333em 1em; +} + +.suggestionsToggleButton { + position: absolute; + right: 0.333333333333333em; + top: 0.333333333333333em; +} + +.suggestionsAddFilter { + right: 9.666666666666667em; +} + +.suggestionsRemoveButton { + position: relative; +} + +.suggestionsContentTable { + width: 100%; + border-spacing: 0; + border-collapse: separate; +} + +.suggestionsSettingsMenuItem { + border-bottom: 1px solid #ddd; +} + +.suggestionsSelectedItem { + border-top: 1px solid #ddd; +} + +.suggestionsSettingsMenuItemLabel { + font-weight: 600; +} + +.suggestionsSettingsMenuItemLabel, .suggestionsItemDescription { + text-align: left; + line-height: 2em; +} + +.suggestionsItemDescription > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.suggestionsItemDescriptionImage, .suggestionsItemDescriptionId { + margin: 0 0.25em; +} + +.suggestionsItemDescriptionImage { + background-repeat: no-repeat; + background-size: contain; + background-position: center; + display: inline-block; + vertical-align: bottom; +} + +.suggestionsItemDescriptionId { + font-weight: 600; + display: inline-block; + min-width: 5.5em; +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.java new file mode 100644 index 00000000000..453455596a1 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.java @@ -0,0 +1,206 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import java.util.Collection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.SuggestOracle.Callback; +import com.google.gwt.user.client.ui.SuggestOracle.Request; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.shared.partials.filter.AbstractAsyncSuggestBoxFilter; +import com.sap.sailing.gwt.home.shared.partials.filter.AbstractFilterWidget; +import com.sap.sailing.gwt.home.shared.partials.filter.AbstractSuggestBoxFilter; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionDataProvider.SuggestionItemsCallback; +import com.sap.sailing.gwt.ui.client.StringMessages; + +public final class SuggestedMultiSelection extends Composite implements SuggestedMultiSelectionDataProvider.Display { + + private static SuggestedMultiSelectionUiBinder uiBinder = GWT.create(SuggestedMultiSelectionUiBinder.class); + + interface SuggestedMultiSelectionUiBinder extends UiBinder> { + } + + @UiField SpanElement headerTitleUi; + @UiField FlowPanel notificationToggleContainerUi; + @UiField(provided = true) AbstractFilterWidget suggestionWidgetUi; + @UiField Button removeAllButtonUi; + @UiField FlowPanel itemContainerUi; + private final SuggestedMultiSelectionDataProvider dataProvider; + private final WidgetProvider widgetProvider; + + private SuggestedMultiSelection(SuggestedMultiSelectionDataProvider dataProvider, + WidgetProvider widgetProvider, String title) { + SuggestedMultiSelectionResources.INSTANCE.css().ensureInjected(); + this.dataProvider = dataProvider; + this.widgetProvider = widgetProvider; + this.suggestionWidgetUi = widgetProvider.getSuggestBoxFilter(new SelectionCallback() { + @Override + public void onSuggestionSelected(T selectedItem) { + SuggestedMultiSelection.this.dataProvider.addSelection(selectedItem); + SuggestedMultiSelection.this.addSelectedItem(selectedItem); + } + }); + initWidget(uiBinder.createAndBindUi(this)); + headerTitleUi.setInnerText(title); + this.updateUiState(); + } + + public SuggestedMultiSelectionNotificationToggle addNotificationToggle(final NotificationCallback callback, + String label) { + final SuggestedMultiSelectionNotificationToggle notification = + new SuggestedMultiSelectionNotificationToggle(label); + notification.toggleButtonUi.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + final boolean enabled = notification.isEnabled(); + callback.onNotificationToggled(enabled); + } + }); + notificationToggleContainerUi.add(notification); + return notification; + } + + @UiHandler("removeAllButtonUi") + void onRemoveAllButtonClicked(ClickEvent event) { + dataProvider.clearSelection(); + itemContainerUi.clear(); + this.updateUiState(); + } + + @Override + public void setSelectedItems(Collection selectedItemsToSet) { + itemContainerUi.clear(); + for (final T item : selectedItemsToSet) { + this.addSelectedItem(item); + } + } + + private void addSelectedItem(final T selectedItem) { + itemContainerUi.add(new SuggestedMultiSelectionItem() { + @Override + protected IsWidget getItemDescriptionWidget() { + return widgetProvider.getItemDescriptionWidget(selectedItem); + } + + @Override + protected void onRemoveItemRequsted() { + dataProvider.removeSelection(selectedItem); + this.removeFromParent(); + updateUiState(); + } + }); + this.updateUiState(); + } + + private void updateUiState() { + removeAllButtonUi.setEnabled(itemContainerUi.getWidgetCount() > 0); + } + + private static abstract class AbstractSuggestedMultiSelectionFilter extends AbstractAsyncSuggestBoxFilter { + private final SuggestedMultiSelectionDataProvider dataProvider; + private final SelectionCallback selectionCallback; + + protected AbstractSuggestedMultiSelectionFilter(SuggestedMultiSelectionDataProvider dataProvider, + SelectionCallback selectionCallback, String placeholderText) { + super(placeholderText); + this.dataProvider = dataProvider; + this.selectionCallback = selectionCallback; + } + + @Override + protected void getSuggestions(final Request request, final Callback callback, + final Iterable queryTokens) { + dataProvider.getSuggestionItems(queryTokens, request.getLimit(), new SuggestionItemsCallback() { + @Override + public void setSuggestionItems(Collection suggestionItems) { + setSuggestions(request, callback, suggestionItems, queryTokens); + } + }); + } + + @Override + protected final void onSuggestionSelected(T selectedItem) { + AbstractSuggestedMultiSelectionFilter.this.clear(); + selectionCallback.onSuggestionSelected(selectedItem); + } + } + + public interface NotificationCallback { + void onNotificationToggled(boolean enabled); + } + + private interface SelectionCallback { + void onSuggestionSelected(T selectedItem); + } + + private interface WidgetProvider { + IsWidget getItemDescriptionWidget(T item); + AbstractSuggestBoxFilter getSuggestBoxFilter(SelectionCallback selectionCallback); + } + + public static SuggestedMultiSelection forCompetitors( + final SuggestedMultiSelectionDataProvider dataProvider, String headerTitle) { + return new SuggestedMultiSelection<>(dataProvider, new WidgetProvider() { + @Override + public IsWidget getItemDescriptionWidget(SimpleCompetitorWithIdDTO item) { + return new SuggestedMultiSelectionCompetitorItemDescription(item); + } + + @Override + public AbstractSuggestBoxFilter getSuggestBoxFilter( + SelectionCallback selectionCallback) { + return new AbstractSuggestedMultiSelectionFilter(dataProvider, + selectionCallback, StringMessages.INSTANCE.add(StringMessages.INSTANCE.competitor())) { + @Override + protected String createSuggestionKeyString(SimpleCompetitorWithIdDTO value) { + return value.getSailID(); + } + + @Override + protected String createSuggestionAdditionalDisplayString(SimpleCompetitorWithIdDTO value) { + return value.getName(); + } + }; + } + }, headerTitle); + } + + public static SuggestedMultiSelection forBoatClasses( + final SuggestedMultiSelectionDataProvider dataProvider, String headerTitle) { + return new SuggestedMultiSelection<>(dataProvider, new WidgetProvider() { + @Override + public IsWidget getItemDescriptionWidget(BoatClassDTO item) { + return new SuggestedMultiSelectionBoatClassItemDescription(item); + } + + @Override + public AbstractSuggestBoxFilter getSuggestBoxFilter( + SelectionCallback selectionCallback) { + return new AbstractSuggestedMultiSelectionFilter(dataProvider, selectionCallback, + StringMessages.INSTANCE.add(StringMessages.INSTANCE.boatClass())) { + @Override + protected String createSuggestionKeyString(BoatClassDTO value) { + return value.getName(); + } + + @Override + protected String createSuggestionAdditionalDisplayString(BoatClassDTO value) { + return null; + } + }; + } + }, headerTitle); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.ui.xml new file mode 100644 index 00000000000..1d36038a970 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelection.ui.xml @@ -0,0 +1,29 @@ + + + + + + + .noResults { + font-weight: bold; + font-style: italic; + padding: 1.2em; + } + + +
+ +
+
+ +
+ + + + +
+ +
+
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassDataProvider.java new file mode 100644 index 00000000000..e1b8d6d075c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassDataProvider.java @@ -0,0 +1,22 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionBoatClassDataProvider.Display; + +public interface SuggestedMultiSelectionBoatClassDataProvider + extends SuggestedMultiSelectionDataProvider { + + void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces); + + void setNotifyAboutResults(boolean notifyAboutResults); + + void initNotifications(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults); + + interface Display extends SuggestedMultiSelectionDataProvider.Display { + + void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces); + + void setNotifyAboutResults(boolean notifyAboutResults); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassItemDescription.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassItemDescription.java new file mode 100644 index 00000000000..ad7f61a03f8 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassItemDescription.java @@ -0,0 +1,30 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.dto.BoatClassDTO; +import com.sap.sailing.gwt.common.client.BoatClassImageResolver; + +class SuggestedMultiSelectionBoatClassItemDescription extends Widget { + + private static LocalUiBinder uiBinder = GWT.create(LocalUiBinder.class); + + interface LocalUiBinder extends UiBinder { + } + + @UiField DivElement imageUi; + @UiField SpanElement nameUi; + + SuggestedMultiSelectionBoatClassItemDescription(BoatClassDTO boatClass) { + setElement(uiBinder.createAndBindUi(this)); + imageUi.getStyle().setBackgroundImage("url('" + BoatClassImageResolver.getBoatClassIconResource( + boatClass.getName()).getSafeUri().asString() + "')"); + nameUi.setInnerText(boatClass.getName()); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassItemDescription.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassItemDescription.ui.xml new file mode 100644 index 00000000000..8c02a60ac3d --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionBoatClassItemDescription.ui.xml @@ -0,0 +1,16 @@ + + + + + + + .imageSize { + width: 2.133333333333333em !important; + height: 2.133333333333333em !important; + } + +
+
+ +
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorDataProvider.java new file mode 100644 index 00000000000..7c817e65861 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorDataProvider.java @@ -0,0 +1,17 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionCompetitorDataProvider.Display; + +public interface SuggestedMultiSelectionCompetitorDataProvider + extends SuggestedMultiSelectionDataProvider { + + void setNotifyAboutResults(boolean notifyAboutResults); + + void initNotifications(boolean notifyAboutResults); + + public static interface Display extends SuggestedMultiSelectionDataProvider.Display { + + void setNotifyAboutResults(boolean notifyAboutResults); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorItemDescription.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorItemDescription.java new file mode 100644 index 00000000000..62e73c79782 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorItemDescription.java @@ -0,0 +1,32 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorDTO; +import com.sap.sailing.gwt.ui.client.FlagImageResolver; + +class SuggestedMultiSelectionCompetitorItemDescription extends Widget { + + private static LocalUiBinder uiBinder = GWT.create(LocalUiBinder.class); + + interface LocalUiBinder extends UiBinder { + } + + @UiField DivElement flagImageUi; + @UiField SpanElement sailIdUi; + @UiField SpanElement nameUi; + + SuggestedMultiSelectionCompetitorItemDescription(SimpleCompetitorDTO competitor) { + setElement(uiBinder.createAndBindUi(this)); + flagImageUi.getStyle().setBackgroundImage("url('" + FlagImageResolver.getFlagImageUri( + competitor.getFlagImageURL(), competitor.getTwoLetterIsoCountryCode()).asString() + "')"); + sailIdUi.setInnerText(competitor.getSailID()); + nameUi.setInnerText(competitor.getName()); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorItemDescription.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorItemDescription.ui.xml new file mode 100644 index 00000000000..d80d0ff1683 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionCompetitorItemDescription.ui.xml @@ -0,0 +1,17 @@ + + + + + + + .imageSize { + width: 1.666666666666667em !important; + height: 2em !important; + } + +
+
+ + +
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionDataProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionDataProvider.java new file mode 100644 index 00000000000..7201772dac6 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionDataProvider.java @@ -0,0 +1,31 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import java.util.Collection; + +import com.google.gwt.view.client.ProvidesKey; +import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionDataProvider.Display; + +public interface SuggestedMultiSelectionDataProvider> extends ProvidesKey { + + void addSelection(T item); + + void removeSelection(T item); + + void clearSelection(); + + void getSuggestionItems(Iterable queryTokens, int limit, final SuggestionItemsCallback callback); + + void addDisplay(D display); + + void persist(); + + void initSelectedItems(Collection selectedItems); + + interface SuggestionItemsCallback { + void setSuggestionItems(Collection suggestionItems); + } + + interface Display { + void setSelectedItems(Collection selectedItems); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionItem.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionItem.java new file mode 100644 index 00000000000..5865055785c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionItem.java @@ -0,0 +1,38 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; + +abstract class SuggestedMultiSelectionItem extends Composite { + + private static LocalUiBinder uiBinder = GWT.create(LocalUiBinder.class); + + interface LocalUiBinder extends UiBinder { + } + + @UiField SimplePanel itemDescriptionContainerUi; + @UiField Button removeItemButtonUi; + + SuggestedMultiSelectionItem() { + initWidget(uiBinder.createAndBindUi(this)); + itemDescriptionContainerUi.setWidget(getItemDescriptionWidget()); + } + + @UiHandler("removeItemButtonUi") + void onRemoveItemButtonClicked(ClickEvent event) { + this.onRemoveItemRequsted(); + } + + protected abstract IsWidget getItemDescriptionWidget(); + + protected abstract void onRemoveItemRequsted(); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionItem.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionItem.ui.xml new file mode 100644 index 00000000000..e3b722d1707 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionItem.ui.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionNotificationToggle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionNotificationToggle.java new file mode 100644 index 00000000000..11a95ab4e6e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionNotificationToggle.java @@ -0,0 +1,54 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HasEnabled; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.common.client.SharedResources; +import com.sap.sailing.gwt.ui.client.StringMessages; + +class SuggestedMultiSelectionNotificationToggle extends Composite implements HasEnabled { + + private static LocalUiBinder uiBinder = GWT.create(LocalUiBinder.class); + + interface LocalUiBinder extends UiBinder { + } + + @UiField SharedResources res; + @UiField Label labelUi; + @UiField Button toggleButtonUi; + private boolean enabled = false; + + SuggestedMultiSelectionNotificationToggle(String label) { + initWidget(uiBinder.createAndBindUi(this)); + labelUi.setText(label); + } + + @UiHandler("toggleButtonUi") + void onToggleButtonClicked(ClickEvent event) { + setEnabled(!isEnabled()); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + this.updateUiState(); + } + + private void updateUiState() { + toggleButtonUi.setStyleName(res.mainCss().buttonred(), !enabled); + toggleButtonUi.setText(enabled ? StringMessages.INSTANCE.enabled() : StringMessages.INSTANCE.disabled()); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionNotificationToggle.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionNotificationToggle.ui.xml new file mode 100644 index 00000000000..67cf57d9084 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionNotificationToggle.ui.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionResources.java new file mode 100644 index 00000000000..05c5bd8ca28 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/multiselection/SuggestedMultiSelectionResources.java @@ -0,0 +1,32 @@ +package com.sap.sailing.gwt.home.shared.partials.multiselection; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; + +public interface SuggestedMultiSelectionResources extends ClientBundle { + + public static final SuggestedMultiSelectionResources INSTANCE = GWT.create(SuggestedMultiSelectionResources.class); + + @Source("SuggestedMultiSelection.gss") + LocalCss css(); + + public interface LocalCss extends CssResource { + String suggestions(); + String suggestionsMobile(); + String suggestionsHeader(); + String suggestionsHeaderTitle(); + String suggestionsContent(); + String suggestionsSettingsMenuItem(); + String suggestionsSettingsMenuItemLabel(); + String suggestionsToggleButton(); + String suggestionsContentToolbar(); + String suggestionsAddFilter(); + String suggestionsRemoveButton(); + String suggestionsContentTable(); + String suggestionsSelectedItem(); + String suggestionsItemDescription(); + String suggestionsItemDescriptionImage(); + String suggestionsItemDescriptionId(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/PlaceTokenPrefixes.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/PlaceTokenPrefixes.java index 301f6a4fa67..fe69be3e459 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/PlaceTokenPrefixes.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/PlaceTokenPrefixes.java @@ -25,6 +25,7 @@ public class PlaceTokenPrefixes { public static final String Imprint = "/imprint/"; public static final String UserProfileDefault = "/user/profile/"; public static final String UserProfileDetails = "/user/profile/details/"; + public static final String UserProfilePreferences = "/user/profile/preferences/"; public static final String UserConfirmation = "/user/confirmation/"; public static final String UserPasswordReset = "/user/passwordreset/"; public static final String UserAuthentication = "/user/auth/"; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/AbstractUserProfilePlace.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/AbstractUserProfilePlace.java index d3cc0b67114..4c4d38ff433 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/AbstractUserProfilePlace.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/AbstractUserProfilePlace.java @@ -22,6 +22,6 @@ public abstract class AbstractUserProfilePlace extends Place implements HasLocat return ""; } - abstract PLACE getRealPlace(); + protected abstract PLACE getRealPlace(); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/preferences/UserProfilePreferencesPlace.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/preferences/UserProfilePreferencesPlace.java new file mode 100644 index 00000000000..9478bc97842 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/preferences/UserProfilePreferencesPlace.java @@ -0,0 +1,18 @@ +package com.sap.sailing.gwt.home.shared.places.user.profile.preferences; + +import com.google.gwt.place.shared.Prefix; +import com.sap.sailing.gwt.home.shared.app.HasMobileVersion; +import com.sap.sailing.gwt.home.shared.places.PlaceTokenPrefixes; +import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; + + +public class UserProfilePreferencesPlace extends AbstractUserProfilePlace implements HasMobileVersion { + + @Prefix(PlaceTokenPrefixes.UserProfilePreferences) + public static class Tokenizer extends AbstractUserProfilePlace.Tokenizer { + @Override + protected UserProfilePreferencesPlace getRealPlace() { + return new UserProfilePreferencesPlace(); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/RegattaOverview.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/RegattaOverview.gwt.xml index ea9bcc3a60c..476d2031cbd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/RegattaOverview.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/RegattaOverview.gwt.xml @@ -42,6 +42,7 @@ + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java index 9816dfb46b1..f5653f8e579 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewEntryPoint.java @@ -1,12 +1,11 @@ package com.sap.sailing.gwt.regattaoverview.client; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.http.client.UrlBuilder; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.DockLayoutPanel; import com.google.gwt.user.client.ui.FlowPanel; @@ -17,19 +16,16 @@ import com.google.gwt.user.client.ui.ScrollPanel; import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication; import com.sap.sailing.gwt.common.client.SharedResources; import com.sap.sailing.gwt.regattaoverview.client.RegattaRaceStatesComponent.EntryHandler; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaOverviewBaseSettings; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSettings; import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint; import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; -import com.sap.sse.gwt.client.URLEncoder; +import com.sap.sse.gwt.settings.SettingsToUrlSerializer; import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication; public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint { - private final static String PARAM_EVENT = "event"; - private final static String PARAM_ONLY_RUNNING_RACES = "onlyrunningraces"; - private final static String PARAM_ONLY_RACES_OF_SAME_DAY = "onlyracesofsameday"; - private final static String PARAM_REGATTA = "regatta"; - private final static String PARAM_COURSE_AREA = "coursearea"; - private final static String PARAM_IGNORE_LOCAL_SETTINGS = "ignoreLocalSettings"; + private static final SettingsToUrlSerializer serializer = new SettingsToUrlSerializer(); private DockLayoutPanel containerPanel; private RaceDetailPanel detailPanel; @@ -61,14 +57,16 @@ public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint { siteHeader.addWidgetToRightSide(clockLabel); containerPanel.addNorth(siteHeader, 75); - String eventIdAsString = Window.Location.getParameter(PARAM_EVENT); - if (eventIdAsString == null) { + RegattaOverviewBaseSettings regattaOverviewSettings = serializer + .deserializeFromCurrentLocation(new RegattaOverviewBaseSettings()); + + if (regattaOverviewSettings.getEvent() == null) { Window.alert("Missing parameter"); return; } createAndAddDetailPanel(); - createAndAddRegattaPanel(UUID.fromString(eventIdAsString)); + createAndAddRegattaPanel(regattaOverviewSettings.getEvent(), regattaOverviewSettings.isIgnoreLocalSettings()); toggleDetailPanel(false); regattaPanel.setEntryClickedHandler(new EntryHandler() { @@ -91,9 +89,8 @@ public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint { // containerPanel.setWidgetHidden(detailPanel, !visibile); } - private void createAndAddRegattaPanel(UUID eventId) { + private void createAndAddRegattaPanel(UUID eventId, boolean ignoreLocalSettings) { RegattaRaceStatesSettings settings = createRegattaRaceStatesSettingsFromURL(); - boolean ignoreLocalSettings = getIgnoreLocalSettingsFromURL(); regattaPanel = new RegattaOverviewPanel(sailingService, this, getStringMessages(), eventId, settings, userAgent, ignoreLocalSettings); regattaPanel.addHandler(new EventDTOLoadedEvent.Handler() { @@ -117,10 +114,6 @@ public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint { containerPanel.add(scrollPanel); } - private boolean getIgnoreLocalSettingsFromURL() { - return Boolean.parseBoolean(Window.Location.getParameter(PARAM_IGNORE_LOCAL_SETTINGS)); - } - private void createAndAddDetailPanel() { detailPanel = new RaceDetailPanel(getStringMessages(), new ClickHandler() { @Override @@ -132,60 +125,12 @@ public class RegattaOverviewEntryPoint extends AbstractSailingEntryPoint { } public static RegattaRaceStatesSettings createRegattaRaceStatesSettingsFromURL() { - List visibleCourseAreas = new ArrayList(); - List visibleRegattas = new ArrayList(); - - boolean showOnlyCurrentlyRunningRaces = Window.Location.getParameter(PARAM_ONLY_RUNNING_RACES) == null - || !Window.Location.getParameter(PARAM_ONLY_RUNNING_RACES).equalsIgnoreCase("false"); - - boolean showOnlyRacesOfSameDay = Window.Location.getParameter(PARAM_ONLY_RACES_OF_SAME_DAY) != null - && Window.Location.getParameter(PARAM_ONLY_RACES_OF_SAME_DAY).equalsIgnoreCase("true"); - - if (Window.Location.getParameterMap().containsKey(PARAM_COURSE_AREA)) { - for (String value : Window.Location.getParameterMap().get(PARAM_COURSE_AREA)) { - visibleCourseAreas.add(UUID.fromString(value)); - } - } - - if (Window.Location.getParameterMap().containsKey(PARAM_REGATTA)) { - visibleRegattas.addAll(Window.Location.getParameterMap().get(PARAM_REGATTA)); - } - - return new RegattaRaceStatesSettings(visibleCourseAreas, visibleRegattas, showOnlyRacesOfSameDay, showOnlyCurrentlyRunningRaces); + return serializer.deserializeFromCurrentLocation(new RegattaRaceStatesSettings()); } - public static String getUrl(UUID eventId, RegattaRaceStatesSettings settings, - boolean isSetVisibleCourseAreasInUrl, boolean isSetVisibleRegattasInUrl) { - String debugParam = Window.Location.getParameter("gwt.codesvr"); - String showOnlyCurrentlyRunningRaces = "&" + PARAM_ONLY_RUNNING_RACES + "=" + (settings.isShowOnlyCurrentlyRunningRaces() ? "true" : "false"); - String showOnlyRacesOfSameDay = "&" + PARAM_ONLY_RACES_OF_SAME_DAY + "=" + (settings.isShowOnlyRacesOfSameDay() ? "true" : "false"); - String ignoreLocalSettings = "&" + PARAM_IGNORE_LOCAL_SETTINGS + "=true"; - - StringBuilder visibleCourseAreas = new StringBuilder(); - if (isSetVisibleCourseAreasInUrl) { - for (UUID visibleCourseArea : settings.getVisibleCourseAreas()) { - visibleCourseAreas.append('&'); - visibleCourseAreas.append(PARAM_COURSE_AREA); - visibleCourseAreas.append('='); - visibleCourseAreas.append(visibleCourseArea.toString()); - } - } - StringBuilder visibleRegattas = new StringBuilder(); - if (isSetVisibleRegattasInUrl) { - for (String visibleRegatta : settings.getVisibleRegattas()) { - visibleRegattas.append('&'); - visibleRegattas.append(PARAM_REGATTA); - visibleRegattas.append('='); - visibleRegattas.append(visibleRegatta); - } - } - String link = URLEncoder.encode("/gwt/RegattaOverview.html?" + PARAM_EVENT+ "=" + eventId.toString() - + visibleCourseAreas.toString() - + visibleRegattas.toString() - + showOnlyCurrentlyRunningRaces - + showOnlyRacesOfSameDay - + ignoreLocalSettings - + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : "")); - return link; + public static String getUrl(UUID eventId, RegattaRaceStatesSettings settings) { + UrlBuilder urlBuilder = serializer.serializeUrlBuilderBasedOnCurrentLocationWithCleanParameters(settings); + serializer.serializeToUrlBuilder(new RegattaOverviewBaseSettings(eventId), urlBuilder); + return urlBuilder.buildString(); } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java index 44e7bca6166..021847ab75b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java @@ -21,6 +21,7 @@ import com.google.gwt.user.client.ui.TabPanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.sap.sailing.gwt.common.client.SharedResources; import com.sap.sailing.gwt.regattaoverview.client.RegattaRaceStatesComponent.EntryHandler; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSettings; import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java index aa2a5289ea3..f90addc4cfc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesComponent.java @@ -20,9 +20,6 @@ import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat; import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONParser; -import com.google.gwt.json.client.JSONValue; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.storage.client.Storage; @@ -53,13 +50,12 @@ import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.racelog.Flags; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSettings; import com.sap.sailing.gwt.ui.client.AnchorCell; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; -import com.sap.sailing.gwt.ui.client.GwtJsonDeSerializer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.ClickableSafeHtmlCell; -import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; @@ -67,6 +63,7 @@ import com.sap.sailing.gwt.ui.shared.RaceGroupSeriesDTO; import com.sap.sailing.gwt.ui.shared.RaceInfoDTO; import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sse.common.Util; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; @@ -74,6 +71,7 @@ import com.sap.sse.gwt.client.celltable.BaseCelltable; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.settings.SettingsToJsonSerializerGWT; /** * This component shows a table displaying the current state of races for a given event. Which races are shown depends @@ -361,8 +359,9 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component return; } final long clientTimeWhenRequestWasSent = System.currentTimeMillis(); - sailingService.getRaceStateEntriesForRaceGroup(eventId, settings.getVisibleCourseAreas(), settings - .getVisibleRegattas(), settings.isShowOnlyCurrentlyRunningRaces(), settings.isShowOnlyRacesOfSameDay(), + sailingService.getRaceStateEntriesForRaceGroup(eventId, Util.asList(settings.getVisibleCourseAreas()), + Util.asList(settings.getVisibleRegattas()), settings.isShowOnlyCurrentlyRunningRaces(), settings + .isShowOnlyRacesOfSameDay(), new MarkedAsyncCallback>( new AsyncCallback>() { @Override @@ -744,24 +743,12 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component @Override public void updateSettings(RegattaRaceStatesSettings newSettings) { - if (settings.getVisibleCourseAreas().isEmpty() - || !settings.getVisibleCourseAreas().equals(newSettings.getVisibleCourseAreas())) { - settings.getVisibleCourseAreas().clear(); - settings.getVisibleCourseAreas().addAll(newSettings.getVisibleCourseAreas()); - fillVisibleCourseAreasInSettingsIfEmpty(); - } - if (settings.getVisibleRegattas().isEmpty() - || !settings.getVisibleRegattas().equals(newSettings.getVisibleRegattas())) { - settings.getVisibleRegattas().clear(); - settings.getVisibleRegattas().addAll(newSettings.getVisibleRegattas()); - fillVisibleRegattasInSettingsIfEmpty(); - } - if (settings.isShowOnlyRacesOfSameDay() != newSettings.isShowOnlyRacesOfSameDay()) { - settings.setShowOnlyRaceOfSameDay(newSettings.isShowOnlyRacesOfSameDay()); - } - if (settings.isShowOnlyCurrentlyRunningRaces() != newSettings.isShowOnlyCurrentlyRunningRaces()) { - settings.setShowOnlyCurrentlyRunningRaces(newSettings.isShowOnlyCurrentlyRunningRaces()); - } + setDefaultCourseAreas(); + settings.setVisibleCourseAreas(newSettings.getVisibleCourseAreas()); + setDefaultRegattas(); + settings.setVisibleRegattas(newSettings.getVisibleRegattas()); + settings.setShowOnlyRaceOfSameDay(newSettings.isShowOnlyRacesOfSameDay()); + settings.setShowOnlyCurrentlyRunningRaces(newSettings.isShowOnlyCurrentlyRunningRaces()); refreshTableWithNewSettings(); storeRegattaRaceStatesSettings(settings); } @@ -777,19 +764,13 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component } } - private void fillVisibleRegattasInSettingsIfEmpty() { - if (settings.getVisibleRegattas().isEmpty() && raceGroupDTOs != null) { - for (RaceGroupDTO raceGroup : raceGroupDTOs) { - settings.getVisibleRegattas().add(raceGroup.getName()); - } - } + private void setDefaultRegattas() { + settings.setDefaultRegattas(raceGroupDTOs); } - private void fillVisibleCourseAreasInSettingsIfEmpty() { - if (settings.getVisibleCourseAreas().isEmpty() && eventDTO != null) { - for (CourseAreaDTO courseArea : eventDTO.venue.getCourseAreas()) { - settings.getVisibleCourseAreas().add(courseArea.id); - } + private void setDefaultCourseAreas() { + if(eventDTO != null) { + settings.setDefaultCourseAreas(eventDTO.venue.getCourseAreas()); } } @@ -806,7 +787,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component @Override public void onEventUpdated(EventDTO event) { eventDTO = event; - fillVisibleCourseAreasInSettingsIfEmpty(); + setDefaultCourseAreas(); refreshTableWithNewSettings(); } @@ -845,7 +826,7 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component // } // } // regattaOverviewTable.redraw(); - fillVisibleRegattasInSettingsIfEmpty(); + setDefaultRegattas(); refreshTableWithNewSettings(); } @@ -855,9 +836,8 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component // delete old value localStorage.removeItem(localStorageRegattaOverviewEventKey); // store settings - GwtJsonDeSerializer serializer = new RegattaRaceStatesSettingsJsonDeSerializer(); - JSONObject settingsAsJson = serializer.serialize(settings); - localStorage.setItem(localStorageRegattaOverviewEventKey, settingsAsJson.toString()); + localStorage.setItem(localStorageRegattaOverviewEventKey, + new SettingsToJsonSerializerGWT().serializeToString(settings)); } } @@ -866,13 +846,8 @@ public class RegattaRaceStatesComponent extends SimplePanel implements Component Storage localStorage = Storage.getLocalStorageIfSupported(); if (localStorage != null) { String jsonAsLocalStore = localStorage.getItem(localStorageRegattaOverviewEventKey); - if (jsonAsLocalStore != null && !jsonAsLocalStore.isEmpty()) { - GwtJsonDeSerializer deserializer = new RegattaRaceStatesSettingsJsonDeSerializer(); - JSONValue value = JSONParser.parseStrict(jsonAsLocalStore); - if (value.isObject() != null) { - loadedSettings = deserializer.deserialize((JSONObject) value); - } - } + loadedSettings = new SettingsToJsonSerializerGWT().deserialize(new RegattaRaceStatesSettings( + eventDTO == null ? null : eventDTO.venue.getCourseAreas(), raceGroupDTOs), jsonAsLocalStore); } return loadedSettings; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettings.java deleted file mode 100644 index 6f576eceb3a..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettings.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.sap.sailing.gwt.regattaoverview.client; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import com.sap.sse.common.settings.AbstractSettings; - -public class RegattaRaceStatesSettings extends AbstractSettings { - private List visibleCourseAreas; - private List visibleRegattas; - - private boolean showOnlyRacesOfSameDay; - private boolean showOnlyCurrentlyRunningRaces; - - public RegattaRaceStatesSettings() { - this.visibleCourseAreas = new ArrayList(); - this.visibleRegattas = new ArrayList(); - this.showOnlyRacesOfSameDay = false; - this.showOnlyCurrentlyRunningRaces = true; - } - - public RegattaRaceStatesSettings(List visibleCourseAreas, List visibleRegattas, - boolean showOnlyRacesOfSameDay, boolean showOnlyCurrentlyRunningRaces) { - this.visibleCourseAreas = visibleCourseAreas; - this.visibleRegattas = visibleRegattas; - this.showOnlyRacesOfSameDay = showOnlyRacesOfSameDay; - this.showOnlyCurrentlyRunningRaces = showOnlyCurrentlyRunningRaces; - } - - public List getVisibleCourseAreas() { - return visibleCourseAreas; - } - - public List getVisibleRegattas() { - return visibleRegattas; - } - - public boolean isShowOnlyRacesOfSameDay() { - return showOnlyRacesOfSameDay; - } - - public void setShowOnlyRaceOfSameDay(boolean newValue) { - showOnlyRacesOfSameDay = newValue; - } - - public boolean isShowOnlyCurrentlyRunningRaces() { - return showOnlyCurrentlyRunningRaces; - } - - public void setShowOnlyCurrentlyRunningRaces(boolean newValue) { - showOnlyCurrentlyRunningRaces = newValue; - } -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsDialogComponent.java index c51f5966fce..98f085dc64a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsDialogComponent.java @@ -17,6 +17,7 @@ import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSettings; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; @@ -240,7 +241,8 @@ public class RegattaRaceStatesSettingsDialogComponent implements SettingsDialogC boolean isShowOnlyRacesOfSameDay = showOnlyRacesOfSameDayCheckBox.getValue(); boolean isShowOnlyCurrentlyRunningRaces = showOnlyCurrentlyRunningRacesCheckBox.getValue(); - return new RegattaRaceStatesSettings(selectedCourseAreas, selectedRegattas, isShowOnlyRacesOfSameDay, isShowOnlyCurrentlyRunningRaces); + return new RegattaRaceStatesSettings(this.courseAreas, selectedCourseAreas, this.raceGroups, selectedRegattas, + isShowOnlyRacesOfSameDay, isShowOnlyCurrentlyRunningRaces); } @Override @@ -263,15 +265,7 @@ public class RegattaRaceStatesSettingsDialogComponent implements SettingsDialogC } private void updateLinkUrl(UUID eventId, RegattaRaceStatesSettings settings) { - boolean isSetVisibleCourseAreasInUrl = true; - boolean isSetVisibleRegattasInUrl = true; - if (settings.getVisibleCourseAreas().size() == courseAreas.size()) { - isSetVisibleCourseAreasInUrl = false; - } - if (settings.getVisibleRegattas().size() == raceGroups.size()) { - isSetVisibleRegattasInUrl = false; - } - resultingLink.setHref(RegattaOverviewEntryPoint.getUrl(eventId, settings, isSetVisibleCourseAreasInUrl, isSetVisibleRegattasInUrl)); + resultingLink.setHref(RegattaOverviewEntryPoint.getUrl(eventId, settings)); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsJsonDeSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsJsonDeSerializer.java deleted file mode 100644 index 7cb7e55da8a..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaRaceStatesSettingsJsonDeSerializer.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.sap.sailing.gwt.regattaoverview.client; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import com.google.gwt.json.client.JSONArray; -import com.google.gwt.json.client.JSONBoolean; -import com.google.gwt.json.client.JSONObject; -import com.google.gwt.json.client.JSONString; -import com.sap.sailing.gwt.ui.client.GwtJsonDeSerializer; - -public class RegattaRaceStatesSettingsJsonDeSerializer implements GwtJsonDeSerializer { - - private static final String FIELD_VISIBLE_COURSE_AREAS = "visibleCourseAreas"; - private static final String FIELD_VISIBLE_REGATTAS = "visibleRegattas"; - private static final String FIELD_SHOW_ONLY_CURRENTLY_RUNNING_RACES = "showOnlyCurrentlyRunningRaces"; - private static final String FIELD_SHOW_ONLY_RACES_OF_SAME_DAY = "showOnlyRacesOfSameDay"; - - @Override - public JSONObject serialize(RegattaRaceStatesSettings settings) { - JSONObject result = new JSONObject(); - result.put(FIELD_SHOW_ONLY_CURRENTLY_RUNNING_RACES, JSONBoolean.getInstance(settings.isShowOnlyCurrentlyRunningRaces())); - result.put(FIELD_SHOW_ONLY_RACES_OF_SAME_DAY, JSONBoolean.getInstance(settings.isShowOnlyRacesOfSameDay())); - - JSONArray visibleCourseAreas = new JSONArray(); - for (int i = 0 ; i < settings.getVisibleCourseAreas().size() ; i++) { - visibleCourseAreas.set(i, new JSONString(settings.getVisibleCourseAreas().get(i).toString())); - } - result.put(FIELD_VISIBLE_COURSE_AREAS, visibleCourseAreas); - - JSONArray visibleRegattas = new JSONArray(); - for (int i = 0 ; i < settings.getVisibleRegattas().size() ; i++) { - visibleRegattas.set(i, new JSONString(settings.getVisibleRegattas().get(i))); - } - result.put(FIELD_VISIBLE_REGATTAS, visibleRegattas); - - return result; - } - - @Override - public RegattaRaceStatesSettings deserialize(JSONObject object) { - if (object == null) { - return null; - } - - JSONBoolean showOnlyCurrentlyRunningRaces = (JSONBoolean) object.get(FIELD_SHOW_ONLY_CURRENTLY_RUNNING_RACES); - JSONBoolean showOnlyRacesOfSameDay = (JSONBoolean) object.get(FIELD_SHOW_ONLY_RACES_OF_SAME_DAY); - - JSONArray jsonVisibleCourseAreas = (JSONArray) object.get(FIELD_VISIBLE_COURSE_AREAS); - List visibleCourseAreas = new ArrayList(); - for (int i = 0 ; i < jsonVisibleCourseAreas.size() ; i++) { - JSONString jsonCourseArea = (JSONString) jsonVisibleCourseAreas.get(i); - visibleCourseAreas.add(UUID.fromString(jsonCourseArea.stringValue())); - } - - JSONArray jsonVisibleRegattas = (JSONArray) object.get(FIELD_VISIBLE_REGATTAS); - List visibleRegattas = new ArrayList(); - for (int i = 0 ; i < jsonVisibleRegattas.size() ; i++) { - JSONString jsonRegatta = (JSONString) jsonVisibleRegattas.get(i); - visibleRegattas.add(jsonRegatta.stringValue()); - } - - return new RegattaRaceStatesSettings(visibleCourseAreas, visibleRegattas, - showOnlyRacesOfSameDay.booleanValue(), showOnlyCurrentlyRunningRaces.booleanValue()); - } - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/Settings.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/Settings.gwt.xml new file mode 100644 index 00000000000..ef964e2a286 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/Settings.gwt.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/EntryPointWithSettingsLinkFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/EntryPointWithSettingsLinkFactory.java new file mode 100644 index 00000000000..9b0ad24458d --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/EntryPointWithSettingsLinkFactory.java @@ -0,0 +1,18 @@ +package com.sap.sailing.gwt.settings.client; + +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaOverviewBaseSettings; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaRaceStatesSettings; +import com.sap.sse.gwt.settings.AbstractEntryPointWithSettingsLinkFactory; + +public class EntryPointWithSettingsLinkFactory extends AbstractEntryPointWithSettingsLinkFactory { + + public static String createRegattaOverviewLink(RegattaOverviewBaseSettings regattaOverviewSettings) { + return createRegattaOverviewLink(regattaOverviewSettings, new RegattaRaceStatesSettings()); + } + + public static String createRegattaOverviewLink(RegattaOverviewBaseSettings regattaOverviewSettings, + RegattaRaceStatesSettings regattaRaceStatesSettings) { + return createEntryPointLink("/gwt/RegattaOverview.html", regattaOverviewSettings, regattaRaceStatesSettings); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/regattaoverview/RegattaOverviewBaseSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/regattaoverview/RegattaOverviewBaseSettings.java new file mode 100644 index 00000000000..ab4c985b218 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/regattaoverview/RegattaOverviewBaseSettings.java @@ -0,0 +1,43 @@ +package com.sap.sailing.gwt.settings.client.regattaoverview; + +import java.util.UUID; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.BooleanSetting; +import com.sap.sse.common.settings.generic.UUIDSetting; + +public final class RegattaOverviewBaseSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -8541790130000694098L; + private transient UUIDSetting event; + private transient BooleanSetting ignoreLocalSettings; + + public RegattaOverviewBaseSettings() { + } + + public RegattaOverviewBaseSettings(String event) { + this(UUID.fromString(event)); + } + + public RegattaOverviewBaseSettings(UUID event) { + this(event, true); + } + + @Override + protected void addChildSettings() { + event = new UUIDSetting("event", this); + ignoreLocalSettings = new BooleanSetting("ignoreLocalSettings", this, false); + } + + public RegattaOverviewBaseSettings(UUID event, boolean ignoreLocalSettings) { + this.event.setValue(event); + this.ignoreLocalSettings.setValue(ignoreLocalSettings); + } + + public UUID getEvent() { + return event.getValue(); + } + + public boolean isIgnoreLocalSettings() { + return ignoreLocalSettings.getValue(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/regattaoverview/RegattaRaceStatesSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/regattaoverview/RegattaRaceStatesSettings.java new file mode 100644 index 00000000000..2a46f55cb01 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/regattaoverview/RegattaRaceStatesSettings.java @@ -0,0 +1,110 @@ +package com.sap.sailing.gwt.settings.client.regattaoverview; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; +import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.BooleanSetting; +import com.sap.sse.common.settings.generic.StringSetSetting; +import com.sap.sse.common.settings.generic.UUIDSetSetting; + +public final class RegattaRaceStatesSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = 7059340304556830062L; + private transient UUIDSetSetting visibleCourseAreas; + private transient StringSetSetting visibleRegattas; + private transient BooleanSetting showOnlyRacesOfSameDay; + private transient BooleanSetting showOnlyCurrentlyRunningRaces; + + public RegattaRaceStatesSettings() { + } + + public RegattaRaceStatesSettings(Iterable defaultCourseAreas, Iterable defaultRaceGroups) { + setDefaultCourseAreas(defaultCourseAreas); + setDefaultRegattas(defaultRaceGroups); + } + + public RegattaRaceStatesSettings(Iterable defaultCourseAreas, Iterable visibleCourseAreas, + Iterable defaultRaceGroups, Iterable visibleRegattas, boolean showOnlyRacesOfSameDay, + boolean showOnlyCurrentlyRunningRaces) { + this(defaultCourseAreas, defaultRaceGroups); + this.visibleCourseAreas.setValues(visibleCourseAreas); + this.visibleRegattas.setValues(visibleRegattas); + this.showOnlyRacesOfSameDay.setValue(showOnlyRacesOfSameDay); + this.showOnlyCurrentlyRunningRaces.setValue(showOnlyCurrentlyRunningRaces); + } + + @Override + protected void addChildSettings() { + visibleCourseAreas = new UUIDSetSetting("visibleCourseAreas", this, true); + visibleRegattas = new StringSetSetting("visibleRegattas", this, true); + showOnlyRacesOfSameDay = new BooleanSetting("showOnlyRacesOfSameDay", this, false); + showOnlyCurrentlyRunningRaces = new BooleanSetting("showOnlyCurrentlyRunningRaces", this, true); + } + + public Iterable getVisibleCourseAreas() { + return visibleCourseAreas.getValues(); + } + + public void setVisibleCourseAreas(Iterable visibleCourseAreas) { + this.visibleCourseAreas.setValues(visibleCourseAreas); + } + + public UUIDSetSetting getVisibleCourseAreaSettings() { + return visibleCourseAreas; + } + + public Iterable getVisibleRegattas() { + return visibleRegattas.getValues(); + } + + public void setVisibleRegattas(Iterable visibleRegattas) { + this.visibleRegattas.setValues(visibleRegattas); + } + + public StringSetSetting getVisibleRegattaSettings() { + return visibleRegattas; + } + + public boolean isShowOnlyRacesOfSameDay() { + return showOnlyRacesOfSameDay.getValue(); + } + + public void setShowOnlyRaceOfSameDay(boolean newValue) { + showOnlyRacesOfSameDay.setValue(newValue); + } + + public boolean isShowOnlyCurrentlyRunningRaces() { + return showOnlyCurrentlyRunningRaces.getValue(); + } + + public void setShowOnlyCurrentlyRunningRaces(boolean newValue) { + showOnlyCurrentlyRunningRaces.setValue(newValue); + } + + public void setDefaultCourseAreas(Iterable defaultCourseAreas) { + if(defaultCourseAreas == null) { + defaultCourseAreas = Collections.emptySet(); + } + Set courseAreaIds = new HashSet<>(); + for (CourseAreaDTO courseArea : defaultCourseAreas) { + courseAreaIds.add(courseArea.id); + } + visibleCourseAreas.setDefaultValues(courseAreaIds); + } + + public void setDefaultRegattas(Iterable defaultRaceGroups) { + if(defaultRaceGroups == null) { + defaultRaceGroups = Collections.emptySet(); + } + Set regattaIds = new HashSet<>(); + for (RaceGroupDTO raceGroup : defaultRaceGroups) { + regattaIds.add(raceGroup.getName()); + } + visibleRegattas.setDefaultValues(regattaIds); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml index 833bd8dba52..58909c1db43 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml @@ -63,6 +63,7 @@ + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventCreateDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventCreateDialog.java index 8f326c00669..6ad13ea3c6c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventCreateDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventCreateDialog.java @@ -39,6 +39,8 @@ public class EventCreateDialog extends EventDialog { endDateBox.ensureDebugId("EndDateTimeBox"); officialWebsiteURLEntryField = createTextBox(null); officialWebsiteURLEntryField.setVisibleLength(50); + baseURLEntryField = createTextBox(null); + baseURLEntryField.setVisibleLength(50); sailorsInfoWebsiteURLEntryFields = createTextBoxesForLocalesAndDefault(Collections. emptyMap()); isPublicCheckBox = createCheckbox(""); isPublicCheckBox.setValue(false); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDetailsComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDetailsComposite.java index 8762b4e5341..44994a011c6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDetailsComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDetailsComposite.java @@ -1,9 +1,7 @@ package com.sap.sailing.gwt.ui.adminconsole; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.client.ui.Anchor; @@ -13,7 +11,8 @@ import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.VerticalPanel; -import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; +import com.sap.sailing.gwt.settings.client.EntryPointWithSettingsLinkFactory; +import com.sap.sailing.gwt.settings.client.regattaoverview.RegattaOverviewBaseSettings; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; @@ -34,6 +33,7 @@ public class EventDetailsComposite extends Composite { private final Label endDate; private final Label isPublic; private final Anchor officialWebsiteURL; + private final Anchor baseURL; private final SimpleAnchorListComposite sailorsInfoWebsiteURLList; private final Anchor eventOverviewURL; private final SimpleStringListComposite courseAreaNamesList; @@ -52,7 +52,7 @@ public class EventDetailsComposite extends Composite { VerticalPanel vPanel = new VerticalPanel(); mainPanel.add(vPanel); - int rows = 16; + int rows = 17; Grid grid = new Grid(rows, 2); vPanel.add(grid); @@ -65,6 +65,7 @@ public class EventDetailsComposite extends Composite { endDate = createLabelAndValueWidget(grid, currentRow++, stringMessages.endDate(), "EndDateLabel"); isPublic = createLabelAndValueWidget(grid, currentRow++, stringMessages.isPublic(), "IsPublicLabel"); officialWebsiteURL = createLabelAndAnchorWidget(grid, currentRow++, stringMessages.eventOfficialWebsiteURL(), "OfficialWebsiteURLLabel"); + baseURL = createLabelAndAnchorWidget(grid, currentRow++, stringMessages.eventBaseURL(), "BaseURLLabel"); sailorsInfoWebsiteURLList = createLabelAndAnchorListWidget(grid, currentRow++, stringMessages.eventSailorsInfoWebsiteURL(), "SailorsInfoWebsiteURLLabel"); eventOverviewURL = createLabelAndAnchorWidget(grid, currentRow++, stringMessages.eventOverviewURL(), "EventOverviewURLLabel"); courseAreaNamesList = createLabelAndValueListWidget(grid, currentRow++, stringMessages.courseAreas(), "CourseAreaValueList"); @@ -134,10 +135,11 @@ public class EventDetailsComposite extends Composite { isPublic.setText(String.valueOf(event.isPublic)); officialWebsiteURL.setText(event.getOfficialWebsiteURL()); officialWebsiteURL.setHref(event.getOfficialWebsiteURL()); + baseURL.setText(event.getBaseURL()); + baseURL.setHref(event.getBaseURL()); sailorsInfoWebsiteURLList.setValues(new ArrayList(event.getSailorsInfoWebsiteURLs().values())); - Map regattaOverviewURLParameters = new HashMap(); - regattaOverviewURLParameters.put("event", event.id.toString()); - String regattaOverviewLink = EntryPointLinkFactory.createRegattaOverviewLink(regattaOverviewURLParameters); + String regattaOverviewLink = EntryPointWithSettingsLinkFactory + .createRegattaOverviewLink(new RegattaOverviewBaseSettings(event.id, false)); eventOverviewURL.setText(regattaOverviewLink); eventOverviewURL.setHref(UriUtils.fromString(regattaOverviewLink)); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java index bd1cfdda6c9..adadf4aae41 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java @@ -23,7 +23,6 @@ import com.google.gwt.user.client.ui.TextArea; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.common.client.GWTLocaleUtil; import com.sap.sailing.gwt.ui.client.DataEntryDialogWithBootstrap; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -33,6 +32,7 @@ import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; import com.sap.sailing.gwt.ui.shared.VenueDTO; +import com.sap.sse.gwt.client.GWTLocaleUtil; import com.sap.sse.gwt.client.IconResources; import com.sap.sse.gwt.client.controls.listedit.GenericStringListInlineEditorComposite; import com.sap.sse.gwt.client.controls.listedit.StringConstantsListEditorComposite; @@ -49,6 +49,7 @@ public abstract class EventDialog extends DataEntryDialogWithBootstrap protected CheckBox isPublicCheckBox; protected UUID id; protected TextBox officialWebsiteURLEntryField; + protected TextBox baseURLEntryField; protected Map sailorsInfoWebsiteURLEntryFields = new HashMap<>(); protected CourseAreaListInlineEditorComposite courseAreaNameList; protected StringConstantsListEditorComposite leaderboardGroupList; @@ -176,6 +177,7 @@ public abstract class EventDialog extends DataEntryDialogWithBootstrap result.setName(nameEntryField.getText()); result.setDescription(descriptionEntryField.getText()); result.setOfficialWebsiteURL(officialWebsiteURLEntryField.getText().trim().isEmpty() ? null : officialWebsiteURLEntryField.getText().trim()); + result.setBaseURL(baseURLEntryField.getText().trim().isEmpty() ? null : baseURLEntryField.getText().trim()); for (Map.Entry sailorsInfoWebsiteUrlEntry : sailorsInfoWebsiteURLEntryFields.entrySet()) { TextBox sailorsInfoWebsiteURLEntryField = sailorsInfoWebsiteUrlEntry.getValue(); String sailorsInfoWebsiteURL = sailorsInfoWebsiteURLEntryField.getText().trim(); @@ -204,24 +206,26 @@ public abstract class EventDialog extends DataEntryDialogWithBootstrap if (additionalWidget != null) { panel.add(additionalWidget); } - Grid formGrid = new Grid(9 + GWTLocaleUtil.getLanguageCountWithDefault(), 2); - formGrid.setWidget(0, 0, new Label(stringMessages.name() + ":")); - formGrid.setWidget(0, 1, nameEntryField); - formGrid.setWidget(1, 0, new Label(stringMessages.description() + ":")); - formGrid.setWidget(1, 1, descriptionEntryField); - formGrid.setWidget(2, 0, new Label(stringMessages.venue() + ":")); - formGrid.setWidget(2, 1, venueEntryField); - formGrid.setWidget(3, 0, new Label(stringMessages.timeZone() + ":")); - formGrid.setWidget(3, 1, new Label(DateAndTimeFormatterUtil.getClientTimeZoneAsGMTString())); - formGrid.setWidget(4, 0, new Label(stringMessages.startDate() + ":")); - formGrid.setWidget(4, 1, startDateBox); - formGrid.setWidget(5, 0, new Label(stringMessages.endDate() + ":")); - formGrid.setWidget(5, 1, endDateBox); - formGrid.setWidget(6, 0, new Label(stringMessages.isPublic() + ":")); - formGrid.setWidget(6, 1, isPublicCheckBox); - formGrid.setWidget(7, 0, new Label(stringMessages.eventOfficialWebsiteURL() + ":")); - formGrid.setWidget(7, 1, officialWebsiteURLEntryField); - int rowIndex = 8; + Grid formGrid = new Grid(10 + GWTLocaleUtil.getLanguageCountWithDefault(), 2); + int rowIndex = 0; + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.name() + ":")); + formGrid.setWidget(rowIndex++, 1, nameEntryField); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.description() + ":")); + formGrid.setWidget(rowIndex++, 1, descriptionEntryField); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.venue() + ":")); + formGrid.setWidget(rowIndex++, 1, venueEntryField); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.timeZone() + ":")); + formGrid.setWidget(rowIndex++, 1, new Label(DateAndTimeFormatterUtil.getClientTimeZoneAsGMTString())); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.startDate() + ":")); + formGrid.setWidget(rowIndex++, 1, startDateBox); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.endDate() + ":")); + formGrid.setWidget(rowIndex++, 1, endDateBox); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.isPublic() + ":")); + formGrid.setWidget(rowIndex++, 1, isPublicCheckBox); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.eventOfficialWebsiteURL() + ":")); + formGrid.setWidget(rowIndex++, 1, officialWebsiteURLEntryField); + formGrid.setWidget(rowIndex, 0, new Label(stringMessages.eventBaseURL() + ":")); + formGrid.setWidget(rowIndex++, 1, baseURLEntryField); for (Map.Entry sailorsInfoWebsiteUrlEntry : sailorsInfoWebsiteURLEntryFields.entrySet()) { String suffix = " ["+ (sailorsInfoWebsiteUrlEntry.getKey() == null ? stringMessages.defaultLocale() + "*" : sailorsInfoWebsiteUrlEntry.getKey()) + "]"; formGrid.setWidget(rowIndex, 0, new Label(stringMessages.eventSailorsInfoWebsiteURL() + suffix + ":")); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java index 60989b5a63f..a426618a9bd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventEditDialog.java @@ -30,6 +30,8 @@ public class EventEditDialog extends EventDialog { id = event.id; officialWebsiteURLEntryField = createTextBox(event.getOfficialWebsiteURL()); officialWebsiteURLEntryField.setVisibleLength(50); + baseURLEntryField = createTextBox(event.getBaseURL()); + baseURLEntryField.setVisibleLength(50); sailorsInfoWebsiteURLEntryFields = createTextBoxesForLocalesAndDefault(event.getSailorsInfoWebsiteURLs()); courseAreaNameList.setValue(new ArrayList<>(event.venue.getCourseAreas())); List leaderboardGroupNames = new ArrayList<>(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index ee0c227e2b0..59d99183862 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -596,9 +596,9 @@ public class EventListComposite extends Composite implements EventsRefresher, Le updatedEvent.startDate, updatedEvent.endDate, updatedEvent.venue, updatedEvent.isPublic, updatedEventLeaderboardGroupIds, updatedEvent.getOfficialWebsiteURL(), - updatedEvent.getSailorsInfoWebsiteURLs(), - updatedEvent.getImages(), updatedEvent.getVideos(), - new AsyncCallback() { + updatedEvent.getBaseURL(), + updatedEvent.getSailorsInfoWebsiteURLs(), updatedEvent.getImages(), + updatedEvent.getVideos(), new AsyncCallback() { @Override public void onFailure(Throwable t) { errorReporter.reportError("Error trying to update sailing event" + oldEvent.getName() + ": " + t.getMessage()); @@ -670,8 +670,8 @@ public class EventListComposite extends Composite implements EventsRefresher, Le courseAreaNames.add(courseAreaDTO.getName()); } sailingService.createEvent(newEvent.getName(), newEvent.getDescription(), newEvent.startDate, newEvent.endDate, - newEvent.venue.getName(), newEvent.isPublic, courseAreaNames, newEvent.getOfficialWebsiteURL(), newEvent.getSailorsInfoWebsiteURLs(), - newEvent.getImages(), newEvent.getVideos(), newEvent.getLeaderboardGroupIds(), new AsyncCallback() { + newEvent.venue.getName(), newEvent.isPublic, courseAreaNames, newEvent.getOfficialWebsiteURL(), newEvent.getBaseURL(), + newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), newEvent.getVideos(), newEvent.getLeaderboardGroupIds(), new AsyncCallback() { @Override public void onFailure(Throwable t) { errorReporter.reportError("Error trying to create new event " + newEvent.getName() + ": " + t.getMessage()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index 573870fdbd0..f6807bb9ab4 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -265,9 +265,9 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat courseAreaNames.add(courseAreaDTO.getName()); } sailingService.createEvent(newEvent.getName(), newEvent.getDescription(), newEvent.startDate, newEvent.endDate, - newEvent.venue.getName(), newEvent.isPublic, courseAreaNames, newEvent.getOfficialWebsiteURL(), newEvent.getSailorsInfoWebsiteURLs(), - newEvent.getImages(), newEvent.getVideos(), - newEvent.getLeaderboardGroupIds(), new AsyncCallback() { + newEvent.venue.getName(), newEvent.isPublic, courseAreaNames, newEvent.getOfficialWebsiteURL(), newEvent.getBaseURL(), + newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), + newEvent.getVideos(), newEvent.getLeaderboardGroupIds(), new AsyncCallback() { @Override public void onFailure(Throwable t) { errorReporter.reportError(stringMessages.errorTryingToCreateNewEvent(newEvent.getName(), t.getMessage())); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java index e531c1754bd..ffc87d27031 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/VideoDialog.java @@ -14,12 +14,12 @@ import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.common.client.GWTLocaleUtil; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.media.MediaConstants; import com.sap.sse.common.Util; import com.sap.sse.common.media.MimeType; import com.sap.sse.gwt.adminconsole.URLFieldWithFileUpload; +import com.sap.sse.gwt.client.GWTLocaleUtil; import com.sap.sse.gwt.client.IconResources; import com.sap.sse.gwt.client.controls.IntegerBox; import com.sap.sse.gwt.client.controls.listedit.GenericStringListInlineEditorComposite; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EntryPointLinkFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EntryPointLinkFactory.java index 0cbc204ee07..8bb1e9d3249 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EntryPointLinkFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EntryPointLinkFactory.java @@ -41,10 +41,6 @@ public class EntryPointLinkFactory extends AbstractEntryPointLinkFactory { return createEntryPointLink("/gwt/Home.html#/event/:eventId="+eventId, parameters); } - public static String createRegattaOverviewLink(Map parameters) { - return createEntryPointLink("/gwt/RegattaOverview.html", parameters); - } - public static String createDashboardLink(Map parameters) { return createEntryPointLink("/dashboards/RibDashboard.html", parameters); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 5d5a8641f58..68083225e50 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -315,13 +315,13 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS Pair resolveImageDimensions(String imageUrlAsString) throws Exception; EventDTO updateEvent(UUID eventId, String eventName, String eventDescription, Date startDate, Date endDate, - VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURL, Map sailorsInfoWebsiteURLsByLocaleName, - Iterable images, Iterable videos) throws Exception; + VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURL, String baseURLAsString, + Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos) throws Exception; EventDTO createEvent(String eventName, String eventDescription, Date startDate, Date endDate, String venue, boolean isPublic, List courseAreaNames, String officialWebsiteURL, - Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, - Iterable videos, Iterable leaderboardGroupIds) throws Exception; + String baseURLAsString, Map sailorsInfoWebsiteURLsByLocaleName, + Iterable images, Iterable videos, Iterable leaderboardGroupIds) throws Exception; void removeEvent(UUID eventId); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index c2a4adf44be..927dbfb59bf 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -407,13 +407,14 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan void removeEvents(Collection eventIds, AsyncCallback asyncCallback); void createEvent(String eventName, String eventDescription, Date startDate, Date endDate, String venue, - boolean isPublic, List courseAreaNames, String officialWebsiteURL, Map sailorsInfoWebsiteURLsByLocaleName, - Iterable images, Iterable videos, Iterable leaderboardGroupIDs, AsyncCallback callback); + boolean isPublic, List courseAreaNames, String officialWebsiteURL, String baseURL, + Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, + Iterable videos, Iterable leaderboardGroupIDs, AsyncCallback callback); void updateEvent(UUID eventId, String eventName, String eventDescription, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURL, - Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos, - AsyncCallback callback); + String baseURL, Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, + Iterable videos, AsyncCallback callback); void resolveImageDimensions(String imageUrlAsString, AsyncCallback> callback); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index f7943bcc5a8..12b39f997ba 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -1133,6 +1133,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String pleaseSelectAScoringResult(); String timeZone(); String eventOfficialWebsiteURL(); + String eventBaseURL(); String eventSailorsInfoWebsiteURL(); String eventOverviewURL(); String eventLogoImageURL(); @@ -1326,7 +1327,9 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String currentFilterSelection(); String notCapableOfGeneratingACodeForIdentifier(); String serverUrl(); - String rotatedFromTrueNorthClickToToggleWindUp(double mappedTrueNorthDeg); + String rotatedFromTrueNorth(double mappedTrueNorthDeg); + String clickToToggleWindUp(); + String clickToToggleWindStreamlets(); String startLineToFirstMarkTriangle(String distanceInMeters); String dataMiningComponentsHaveBeenUpdated(); String dataMiningComponentsNeedReloadDialogMessage(); @@ -1380,6 +1383,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String regattaQuickfinder(); String resultsQuickfinder(); String racesQuickfinder(); + String profileQuickfinder(); String statistics(); String impressions(); String notAvailableOnMobileMessage(); @@ -1623,4 +1627,12 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String startAnalysis(); String winningLanes(); String maximumNumberOfDiscards(); + String userNotificationsOnlyIfMailAddressIsVerified(); + String enabled(); + String disabled(); + String favoritesAndNotifications(); + String favoriteCompetitors(); + String favoriteBoatClasses(); + String notificationAboutNewResults(); + String notificationAboutUpcomingRaces(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 7a38dc94963..600193f46b7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -1106,6 +1106,7 @@ advancePassId=Advance Pass ID pleaseSelectAScoringResult=Please select a scoring result... timeZone=Time zone eventOfficialWebsiteURL=Official Website URL +eventBaseURL=Event''s Base URL (http://something.sapsailing.com) eventSailorsInfoWebsiteURL=Sailors Info Website URL eventOverviewURL=Event Overview URL eventLogoImageURL=Logo Image URL @@ -1287,7 +1288,9 @@ filterBy=Filter by currentFilterSelection=Current filter selection notCapableOfGeneratingACodeForIdentifier=I''m not capable of generating a code for this identifier. serverUrl=Server URL -rotatedFromTrueNorthClickToToggleWindUp=Rotated {0} degrees from true north.\nTap/click to toggle between wind-up and north-up map display +rotatedFromTrueNorth=Rotated {0} degrees from true north. +clickToToggleWindUp=Tap/click to toggle between wind-up and north-up map display +clickToToggleWindStreamlets=Tap/click to show or hide the wind streamlets startLineToFirstMarkTriangle=Start to first mark ({0}m) dataMiningComponentsHaveBeenUpdated=The Data-Mining components have been updated dataMiningComponentsNeedReloadDialogMessage=Click Reload to reload the components now. This will discard the current displayed data and run a default query.\nClick Close to do nothing. The Data-Mining won''t work correctly, until the components have been reloaded. @@ -1348,6 +1351,7 @@ eventQuickfinder=Event Quickfinder regattaQuickfinder=Regatta Quickfinder resultsQuickfinder=Results Quickfinder racesQuickfinder=Races Quickfinder +profileQuickfinder=Profile Quickfinder statistics=Statistics impressions=Impressions notAvailableOnMobileMessage=This content is not available on mobile.\nSwitch to the desktop version? @@ -1585,6 +1589,14 @@ selectFromRacesWithOverlappingTimeRange=Select from the races whose time range o finishTime=Finish at: {0} showCompetitorCharts=Show competitor chart showWindChart=Show wind chart +userNotificationsOnlyIfMailAddressIsVerified=Due to privacy reasons, mail notifications require that you enter your mail address on the user profile page and verify this mail address. +enabled=Enabled +disabled=Disabled +favoritesAndNotifications=Favorites/Notifications +favoriteCompetitors=Favorite competitors +favoriteBoatClasses=Favorite boat classes +notificationAboutNewResults=Send notifications about new results +notificationAboutUpcomingRaces=Send notifications about upcoming races currentRideHeightInMeters=Ride Height currentRideHeightInMetersTooltip=The current Foiling ride height currentOrAverageRideHeightInMeters=Ride Height (\u2205 at end) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 034a21dec5f..a5308e1d0e8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1096,6 +1096,7 @@ advancePassId=Pass ID erhöhen pleaseSelectAScoringResult=Bitte ein Ergebnis auswählen... timeZone=Zeitzone eventOfficialWebsiteURL=URL der offiziellen Veranstaltungswebseite +eventBaseURL=Stamm-URL der Veranstaltung (http://something.sapsailing.com) eventSailorsInfoWebsiteURL=URL zur Webseite mit Seglerinfos eventOverviewURL=URL der Veranstaltungsübersicht eventLogoImageURL=URL des Veranstaltungslogos @@ -1275,7 +1276,9 @@ filterBy=Gefiltert nach currentFilterSelection=Aktuelle Filter-Selektion notCapableOfGeneratingACodeForIdentifier=Für diesen Identifier kann kein Barcode generiert werden. serverUrl=Server URL -rotatedFromTrueNorthClickToToggleWindUp={0}° gegenüber rechtweisend Nord rotiert.\nKlick wechselt zwischen Kartenanzeige "Wind oben" und "Nord oben" +rotatedFromTrueNorth={0}° gegenüber rechtweisend Nord rotiert. +clickToToggleWindUp=Klick wechselt zwischen Kartenanzeige "Wind oben" und "Nord oben" +clickToToggleWindStreamlets=Klick wechselt der Anzeige der Windstreamlets startLineToFirstMarkTriangle=Vom Start zur nächsten Bahnmarke ({0}m) dataMiningComponentsHaveBeenUpdated=Die Data-Mining Komponenten wurden aktualisiert dataMiningComponentsNeedReloadDialogMessage=Klicke auf Neu Laden, um die Komponenten jetzt neu zu laden. Die aktuell angezeigten Daten gehen dabei verloren und eine Standard-Query wird ausgeführt.\nKlicke auf Schließen, um nichts zu tun. Das Data-Mining wird nicht korrekt funktionieren, bis die Komponenten neu geladen wurden. @@ -1336,6 +1339,7 @@ eventQuickfinder=Veranstaltung-Schnellauswahl regattaQuickfinder=Regatta-Schnellauswahl resultsQuickfinder=Ergebnis-Schnellauswahl racesQuickfinder=Wettfahrt-Schnellauswahl +profileQuickfinder=Profil-Schnellauswahl statistics=Statistiken impressions=Eindrücke notAvailableOnMobileMessage=Diese Ansicht ist noch nicht für die Mobile bzw. Smartphone Ansicht umgesetzt.\nWollen Sie die Desktop Ansicht besuchen? @@ -1570,6 +1574,14 @@ selectFromRacesWithOverlappingTimeRange=Das oder die Rennen aus der Liste derer finishTime=Ende um: {0} showCompetitorCharts=Teilnehmer-Diagramme zeigen showWindChart=Windiagramm zeigen +userNotificationsOnlyIfMailAddressIsVerified=Aus Gründen des Datenschutzes können Benachrichtigungen nur verwendet werden, wenn im Benutzerprofil eine gültige Mail-Adresse angegeben und diese verfifiziert wurde. +enabled=Aktiviert +disabled=Deaktiviert +favoritesAndNotifications=Favoriten/Benachrichtigungen +favoriteCompetitors=Favorisierte Teilnehmer +favoriteBoatClasses=Favorisierte Bootsklassen +notificationAboutNewResults=Über neue Ergebnisse informieren +notificationAboutUpcomingRaces=Über demnächst stattfindende Rennen informieren currentRideHeightInMeters=Fahrhöhe currentRideHeightInMetersTooltip=Die aktuelle Fahrhöhe (Foiling). currentOrAverageRideHeightInMeters=Fahrhöhe (\u2205 nach Abschluss) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index c944a16b192..5eeaabb85b1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -1283,7 +1283,9 @@ filterBy=フィルタキー currentFilterSelection=現在のフィルタ選択 notCapableOfGeneratingACodeForIdentifier=この ID のコードを生成することができません。 serverUrl=サーバ URL -rotatedFromTrueNorthClickToToggleWindUp=真北から {0} 度を回転。\nタップ/クリックしてウィンドアップとノースアップの間のマップ表示を切替 +rotatedFromTrueNorth=真北から {0} 度を回転。 +clickToToggleWindUp=タップ/クリックしてウィンドアップとノースアップの間のマップ表示を切替 +clickToToggleWindStreamlets=Tap/click to show or hide the wind streamlets startLineToFirstMarkTriangle=スタートから第一マークまで ({0}m) dataMiningComponentsHaveBeenUpdated=データマイニングコンポーネントが更新されました dataMiningComponentsNeedReloadDialogMessage=リロードをクリックして、コンポーネントをリロードします。これにより、現在表示されているデータが破棄され、デフォルトクエリが実行されます。\n何も行わない場合は閉じるをクリックします。データマイニングは、コンポーネントがリロードされるまでは正しく機能しません。 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 3cd1c33c2a5..245bf1ca9eb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -1283,7 +1283,9 @@ filterBy=Фильтровать по currentFilterSelection=Текущий выбор фильтра notCapableOfGeneratingACodeForIdentifier=Я не могу сгенерировать код для этого идентификатора. serverUrl=URL-адрес сервера -rotatedFromTrueNorthClickToToggleWindUp=Повернуто на {0} град. от истинного севера.\nКоснитесь/щелкните для переключения между отображением карты с ветром или севером вверху +rotatedFromTrueNorth=Повернуто на {0} град. от истинного севера. +clickToToggleWindUp=Коснитесь/щелкните для переключения между отображением карты с ветром или севером вверху +clickToToggleWindStreamlets=Tap/click to show or hide the wind streamlets startLineToFirstMarkTriangle=От старта до первой отметки ({0} м) dataMiningComponentsHaveBeenUpdated=Компоненты добычи данных обновлены. dataMiningComponentsNeedReloadDialogMessage=Щелкните "Перезагрузить", чтобы перезагрузить компоненты сейчас. Текущее отображение данных будет с##рошено с выполнением запроса по умолчанию. Добыча данных не будет работать правильно до перезагрузки компонентов. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index eb068601f84..6797c085fa4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -1283,7 +1283,9 @@ filterBy=筛选方式 currentFilterSelection=当前筛选器选择 notCapableOfGeneratingACodeForIdentifier=无法为此标识符生成代码。 serverUrl=服务器 URL -rotatedFromTrueNorthClickToToggleWindUp=自真北方向旋转{0}度。\n点按/点击在风向向上和北向上地图显示之间切换 +rotatedFromTrueNorth=自真北方向旋转{0}度。 +clickToToggleWindUp=点按/点击在风向向上和北向上地图显示之间切换 +clickToToggleWindStreamlets=Tap/click to show or hide the wind streamlets startLineToFirstMarkTriangle=开始经过第一个标志 ({0}m) dataMiningComponentsHaveBeenUpdated=数据挖掘组件已更新 dataMiningComponentsNeedReloadDialogMessage=点击“重新加载”,立即重新加载组件。 这将放弃当前显示的数据,并运行默认查询。\n点击“关闭”不执行任何操作。 在重新加载组件前,数据挖掘不能正常工作。 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties index eb068601f84..6797c085fa4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties @@ -1283,7 +1283,9 @@ filterBy=筛选方式 currentFilterSelection=当前筛选器选择 notCapableOfGeneratingACodeForIdentifier=无法为此标识符生成代码。 serverUrl=服务器 URL -rotatedFromTrueNorthClickToToggleWindUp=自真北方向旋转{0}度。\n点按/点击在风向向上和北向上地图显示之间切换 +rotatedFromTrueNorth=自真北方向旋转{0}度。 +clickToToggleWindUp=点按/点击在风向向上和北向上地图显示之间切换 +clickToToggleWindStreamlets=Tap/click to show or hide the wind streamlets startLineToFirstMarkTriangle=开始经过第一个标志 ({0}m) dataMiningComponentsHaveBeenUpdated=数据挖掘组件已更新 dataMiningComponentsNeedReloadDialogMessage=点击“重新加载”,立即重新加载组件。 这将放弃当前显示的数据,并运行默认查询。\n点击“关闭”不执行任何操作。 在重新加载组件前,数据挖掘不能正常工作。 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java index aa92c3f2602..ebc0d0e9371 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java @@ -1,6 +1,8 @@ package com.sap.sailing.gwt.ui.client.shared.racemap; import com.google.gwt.canvas.client.Canvas; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Label; @@ -26,18 +28,35 @@ public class CombinedWindPanel extends FlowPanel { private RaceMapStyle raceMapStyle; private final CoordinateSystem coordinateSystem; - public CombinedWindPanel(RaceMapImageManager theRaceMapResources, RaceMapStyle raceMapStyle, + public CombinedWindPanel(final RaceMap map, RaceMapImageManager theRaceMapResources, RaceMapStyle raceMapStyle, StringMessages stringMessages, CoordinateSystem coordinateSystem) { - raceMapStyle.ensureInjected(); this.stringMessages = stringMessages; this.coordinateSystem = coordinateSystem; this.raceMapResources = theRaceMapResources; this.raceMapStyle = raceMapStyle; addStyleName(raceMapStyle.raceMapIndicatorPanel()); + addStyleName(raceMapStyle.combinedWindPanel()); transformer = raceMapResources.getCombinedWindIconTransformer(); canvas = transformer.getCanvas(); canvas.addStyleName(this.raceMapStyle.raceMapIndicatorPanelCanvas()); add(canvas); + canvas.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + RaceMapSettings oldRaceMapSettings = map.getSettings(); + boolean newShowStreamletsOverlaySetting = !oldRaceMapSettings.isShowWindStreamletOverlay(); + + final RaceMapSettings newRaceMapSettings = new RaceMapSettings(oldRaceMapSettings.getZoomSettings(), + oldRaceMapSettings.getHelpLinesSettings(), oldRaceMapSettings.getTransparentHoverlines(), + oldRaceMapSettings.getHoverlineStrokeWeight(), oldRaceMapSettings.getTailLengthInMilliseconds(), oldRaceMapSettings.isWindUp(), + oldRaceMapSettings.getBuoyZoneRadiusInMeters(), oldRaceMapSettings.isShowOnlySelectedCompetitors(), + oldRaceMapSettings.isShowSelectedCompetitorsInfo(), oldRaceMapSettings.isShowWindStreamletColors(), + newShowStreamletsOverlaySetting, oldRaceMapSettings.isShowSimulationOverlay(), + oldRaceMapSettings.isShowMapControls(), oldRaceMapSettings.getManeuverTypesToShow(), + oldRaceMapSettings.isShowDouglasPeuckerPoints()); + map.updateSettings(newRaceMapSettings); + } + }); textLabel = new Label(""); textLabel.addStyleName(this.raceMapStyle.raceMapIndicatorPanelTextLabel()); add(textLabel); @@ -53,7 +72,8 @@ public class CombinedWindPanel extends FlowPanel { double rotationDegOfWindSymbol = windDTO.dampenedTrueWindBearingDeg; transformer.drawTransformedImage(coordinateSystem.mapDegreeBearing(rotationDegOfWindSymbol), 1.0); String title = stringMessages.wind() + ": " + Math.round(windFromDeg) + " " - + stringMessages.degreesShort() + " (" + WindSourceTypeFormatter.format(windSource, stringMessages) + ")"; + + stringMessages.degreesShort() + " (" + WindSourceTypeFormatter.format(windSource, stringMessages) + ")" + + + '\n'+ stringMessages.clickToToggleWindStreamlets(); canvas.setTitle(title); textLabel.setText(numberFormat.format(speedInKnots) + " " + stringMessages.knotsUnit()); if (!isVisible()) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.css index 9c8056bcd53..21e42e365db 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.css @@ -26,4 +26,6 @@ .combinedWindPanel {} -.trueNorthIndicator {} \ No newline at end of file +.trueNorthIndicatorPanel { + margin-top: 10px; +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 8fc640588bc..031347fa9ff 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -355,8 +355,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe private final CombinedWindPanel combinedWindPanel; - private final TrueNorthIndicatorPanel trueNorthIndicatorPanel; + private final FlowPanel topLeftControlsWrapperPanel; private final AsyncActionsExecutor asyncActionsExecutor; @@ -447,10 +447,15 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe panelForLeftHeaderLabels = new AbsolutePanel(); panelForRightHeaderLabels = new AbsolutePanel(); initializeData(settings.isShowMapControls(), showHeaderPanel); - combinedWindPanel = new CombinedWindPanel(raceMapImageManager, raceMapResources.raceMapStyle(), stringMessages, coordinateSystem); + RaceMapStyle raceMapStyle = raceMapResources.raceMapStyle(); + raceMapStyle.ensureInjected(); + combinedWindPanel = new CombinedWindPanel(this, raceMapImageManager, raceMapStyle, stringMessages, coordinateSystem); combinedWindPanel.setVisible(false); - trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, raceMapResources.raceMapStyle(), stringMessages, coordinateSystem); + trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, raceMapStyle, stringMessages, coordinateSystem); trueNorthIndicatorPanel.setVisible(true); + topLeftControlsWrapperPanel = new FlowPanel(); + topLeftControlsWrapperPanel.add(combinedWindPanel); + topLeftControlsWrapperPanel.add(trueNorthIndicatorPanel); orientationChangeInProgress = false; mapFirstZoomDone = false; // TODO bug 494: reset zoom settings to user preferences @@ -552,8 +557,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe Image sapLogo = createSAPLogo(); RaceMap.this.add(sapLogo); } - map.setControls(ControlPosition.LEFT_TOP, combinedWindPanel); - map.setControls(ControlPosition.LEFT_TOP, trueNorthIndicatorPanel); + + map.setControls(ControlPosition.LEFT_TOP, topLeftControlsWrapperPanel); adjustLeftControlsIndent(); RaceMap.this.raceMapImageManager.loadMapIcons(map); @@ -2656,18 +2661,16 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe getRightHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); // Adjust combined wind and true north indicator panel indent, based on the RaceMap height - if (combinedWindPanel.getParent() != null && trueNorthIndicatorPanel.getParent() != null) { + if (topLeftControlsWrapperPanel.getParent() != null) { this.adjustLeftControlsIndent(); } } private void adjustLeftControlsIndent() { - combinedWindPanel.getParent().setStyleName("CombinedWindPanelParentDiv"); - trueNorthIndicatorPanel.getParent().setStyleName("TrueNorthIndicatorPanelParentDiv"); + topLeftControlsWrapperPanel.getParent().setStyleName("TopLeftControlsWrapperPanelParentDiv"); String leftControlsIndentStyle = getLeftControlsIndentStyle(); if (leftControlsIndentStyle != null) { - combinedWindPanel.getParent().addStyleName(leftControlsIndentStyle); - trueNorthIndicatorPanel.getParent().addStyleName(leftControlsIndentStyle); + topLeftControlsWrapperPanel.getParent().addStyleName(leftControlsIndentStyle); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapStyle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapStyle.java index 4a2179a296b..b8417bd9206 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapStyle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapStyle.java @@ -17,5 +17,5 @@ public interface RaceMapStyle extends CssResource { public String raceMapIndicatorPanelTextLabel(); public String raceMapIndicatorPanelCanvas(); public String combinedWindPanel(); - public String trueNorthIndicator(); + public String trueNorthIndicatorPanel(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/TrueNorthIndicatorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/TrueNorthIndicatorPanel.java index 099cc503058..48cd14e3424 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/TrueNorthIndicatorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/TrueNorthIndicatorPanel.java @@ -35,7 +35,7 @@ public class TrueNorthIndicatorPanel extends FlowPanel { this.raceMapResources = theRaceMapResources; this.raceMapStyle = raceMapStyle; addStyleName(raceMapStyle.raceMapIndicatorPanel()); - addStyleName(raceMapStyle.trueNorthIndicator()); + addStyleName(raceMapStyle.trueNorthIndicatorPanel()); transformer = raceMapResources.getTrueNorthIndicatorIconTransformer(); canvas = transformer.getCanvas(); canvas.addStyleName(this.raceMapStyle.raceMapIndicatorPanelCanvas()); @@ -65,7 +65,8 @@ public class TrueNorthIndicatorPanel extends FlowPanel { protected void redraw() { final double mappedTrueNorthDeg = coordinateSystem.mapDegreeBearing(0); transformer.drawTransformedImage(mappedTrueNorthDeg, 1.0); - String title = stringMessages.rotatedFromTrueNorthClickToToggleWindUp(Math.round(mappedTrueNorthDeg)); + String title = stringMessages.rotatedFromTrueNorth(Math.round(mappedTrueNorthDeg)) + '\n' + + stringMessages.clickToToggleWindUp(); canvas.setTitle(title); NumberFormat numberFormat = NumberFormat.getFormat("0.0"); textLabel.setText(mappedTrueNorthDeg == 0 ? "N" : numberFormat.format(mappedTrueNorthDeg)); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java index 55d4569c087..48b0a599fa9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java @@ -3,11 +3,13 @@ package com.sap.sailing.gwt.ui.leaderboard; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; import com.google.gwt.cell.client.Cell; import com.google.gwt.cell.client.Cell.Context; +import com.google.gwt.core.client.GWT; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.cellview.client.CellTable; @@ -26,7 +28,29 @@ import com.sap.sailing.gwt.ui.client.shared.controls.AbstractSortableColumnWithM */ public abstract class ExpandableSortableColumn extends LeaderboardSortableColumnWithMinMax { private boolean enableExpansion; + + /** + * Set when the {@link #changeExpansionState(boolean)} method begins with its work. Three things can happen: + *
    + *
  1. the column is to be collapsed. This can happen synchronously, and the flag is cleared again before the method + * returns
  2. + *
  3. the column is to be expanded and all information required for this is already available; again, this type + * of request can be handled synchronously, and the flag is cleared before the method returns
  4. + *
  5. the column is to be expanded but not all information required is available yet; it has to be requested using + * an asynchronous call; the method returns with the flag still set, and the callback will clear it when done
  6. + *
+ */ private boolean togglingInProcess; + + /** + * If {@link #togglingInProcess} is {@code true} and a call to {@link #changeExpansionState(boolean)} is received, + * instead of running the method, the request is "queued" only by appending the desired expansion state. When the + * asynchronous task completes the running toggle action, the first request is picked from the list and + * {@link #changeExpansionState(boolean)} is called. This way, overlapping asynchronous requests triggered by calls + * to {@link #changeExpansionState(boolean)} are avoided. + */ + private final LinkedList queuedToggleRequests; + private boolean suppressSortingOnce; private final LeaderboardPanel leaderboardPanel; private final Map> detailColumnsMap; @@ -51,6 +75,7 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol SortingOrder preferredSortingOrder, StringMessages stringConstants, String detailHeaderStyle, String detailColumnStyle, List detailSelection, DisplayedLeaderboardRowsProvider displayedLeaderboardRowsProvider) { super(cell, preferredSortingOrder, displayedLeaderboardRowsProvider); + this.queuedToggleRequests = new LinkedList<>(); this.enableExpansion = enableExpansion; this.leaderboardPanel = leaderboardPanel; this.detailSelection = detailSelection; @@ -119,7 +144,7 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol /** * Determines the direct and transitive child columns that due to the current expansion state should be - * visible. Note that for columns not currently visible or currently being expanded (see {@link #toggleExpansion()}), + * visible. Note that for columns not currently visible or currently being expanded (see {@link #changeExpansionState(boolean)}), * the column collection returned does not necessarily contain only columns really part of the {@link CellTable} * used to display this column. */ @@ -146,46 +171,73 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol * * Precondition: this column must currently be contained in the {@link CellTable} showing the * {@link #leaderboardPanel leaderboard}. + *

+ * + * When the method starts to toggle the expansion state, it sets the {@link #togglingInProcess} flag. When the + * method returns the {@link #isExpanded()} method yields the new expansion state. However, when the column contents + * need to be loaded asynchronously first, the expansion will not have affected the table structure yet and is still + * "pending." This state is indicated by the {@link #isTogglingInProcess()} method returning {@code true} after this + * method has returned. Only after the asynchronous data loading has happened and the table has actually been + * updated to reflect the expanded state, the {@link #isTogglingInProcess()} will then again return {@code false}. + * + * @param expand + * if {@code true} the column will be expanded if not already expanded, if {@code false} the column will + * be collapsed if not already collapsed */ - public void toggleExpansion() { + public void changeExpansionState(boolean expand) { if (isExpansionEnabled()) { - final boolean oldBusyState = getLeaderboardPanel().isBusy(); - getLeaderboardPanel().setBusyState(true); - setTogglingInProcess(true); - final CellTable table = getLeaderboardPanel().getLeaderboardTable(); - if (isExpanded()) { - for (AbstractSortableColumnWithMinMax column : getAllVisibleChildren()) { - getLeaderboardPanel().removeColumn(column); // removes only the children currently displayed - } - getLeaderboardPanel().setBusyState(false); - setTogglingInProcess(false); - // important: toggle expanded state after asking for all visible children - setExpanded(!isExpanded()); + if (isTogglingInProcess()) { + queuedToggleRequests.add(expand); } else { - // important: toggle expanded state before asking for all visible children - setExpanded(!isExpanded()); - ensureExpansionDataIsLoaded(new Runnable() { - public void run() { - // column may have been collapsed meanwhile, so that the columns must not be added to the table right now - if (isExpanded()) { - int insertIndex = table.getColumnIndex(ExpandableSortableColumn.this); - // The check "insertIndex != -1" is necessary, because the child-columns might be deleted - // asynchronously while toggling the columns. - if (insertIndex != -1) { - insertIndex++; - for (AbstractSortableColumnWithMinMax column : getAllVisibleChildren()) { - column.updateMinMax(); - if (table.getColumnIndex(column) < 0) { - getLeaderboardPanel().insertColumn(insertIndex++, column); + final boolean oldBusyState = getLeaderboardPanel().isBusy(); + getLeaderboardPanel().setBusyState(true); + setTogglingInProcess(true); + final CellTable table = getLeaderboardPanel().getLeaderboardTable(); + if (table == null) { + GWT.log("WARNING: leaderborad table is null"); + } else { + if (!expand) { // collapse + if (isExpanded()) { // but only if currently expanded + for (AbstractSortableColumnWithMinMax column : getAllVisibleChildren()) { + getLeaderboardPanel().removeColumn(column); // removes only the children currently displayed + } + getLeaderboardPanel().setBusyState(false); + setTogglingInProcess(false); + // important: toggle expanded state after asking for all visible children + setExpanded(!isExpanded()); + } + } else { // expand + if (!isExpanded()) { // but only if not expanded yet + // important: toggle expanded state before asking for all visible children + setExpanded(!isExpanded()); + ensureExpansionDataIsLoaded(new Runnable() { + public void run() { + // column may have been collapsed meanwhile, so that the columns must not be added to the table right now + if (isExpanded()) { + int insertIndex = table.getColumnIndex(ExpandableSortableColumn.this); + // The check "insertIndex != -1" is necessary, because the child-columns might be deleted + // asynchronously while toggling the columns. + if (insertIndex != -1) { + insertIndex++; + for (AbstractSortableColumnWithMinMax column : getAllVisibleChildren()) { + column.updateMinMax(); + if (table.getColumnIndex(column) < 0) { + getLeaderboardPanel().insertColumn(insertIndex++, column); + } + } + getLeaderboardPanel().getLeaderboardTable().redraw(); + } + } + getLeaderboardPanel().setBusyState(oldBusyState); + setTogglingInProcess(false); + if (!queuedToggleRequests.isEmpty()) { + changeExpansionState(queuedToggleRequests.remove(0)); } } - getLeaderboardPanel().getLeaderboardTable().redraw(); - } + }); } - getLeaderboardPanel().setBusyState(oldBusyState); - setTogglingInProcess(false); } - }); + } } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java index ccc153dd378..2ba5d3184ee 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java @@ -423,7 +423,7 @@ public class LeaderboardPanel extends SimplePanel implements Component expandableSortableColumn : columnsToExpandAgain) { - expandableSortableColumn.toggleExpansion(); + expandableSortableColumn.changeExpansionState(/* expand */ true); } if (newSettings.getNameOfRaceToSort() != null) { final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings @@ -2254,7 +2254,7 @@ public class LeaderboardPanel extends SimplePanel implements Component expandableColumn = (ExpandableSortableColumn) c; if (expandableColumn.isExpanded()) { // remove expanded child columns from the leaderboard... - expandableColumn.toggleExpansion(); + expandableColumn.changeExpansionState(/* expand */ false); // them remember that column c was expanded: expandableColumn.setExpanded(true); } @@ -2361,7 +2361,7 @@ public class LeaderboardPanel extends SimplePanel implements Component> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.toggleExpansion(); + columnToCollapseAndExpandAgain.changeExpansionState(/* expand */ false); } competitorSelectionProvider.setCompetitors(leaderboard.competitors, /* listenersNotToNotify */this); if (!initialCompetitorFilterHasBeenApplied) { @@ -2373,7 +2373,7 @@ public class LeaderboardPanel extends SimplePanel implements Component columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.toggleExpansion(); + columnToCollapseAndExpandAgain.changeExpansionState(/* expand */ true); } adjustDelayToLive(); final Map rowsToDisplay = getRowsToDisplay(); @@ -2417,7 +2417,7 @@ public class LeaderboardPanel extends SimplePanel implements Component expandableSortableColumn = (ExpandableSortableColumn) c; if (!expandableSortableColumn.isExpanded()) { - expandableSortableColumn.toggleExpansion(); + expandableSortableColumn.changeExpansionState(/* expand */ true); autoExpandPerformedOnce = true; } } @@ -2683,7 +2683,7 @@ public class LeaderboardPanel extends SimplePanel implements Component { @Override public void execute(SafeHtml object) { - column.toggleExpansion(); + column.changeExpansionState(/* expand */ !column.isExpanded()); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java index 8fd25bf9484..ef621e840e3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java @@ -148,6 +148,7 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI @Override public void onSuccess(LeaderboardDTO result) { leaderboardForSpecificTimePoint = result; + getLeaderboardPanel().updateLeaderboard(result); trigger(); } }; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 7a137dd59a4..3bd7e4992e4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3398,11 +3398,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public List getEvents() throws MalformedURLException { - String requestBaseURL = getRequestBaseURL().toString(); List result = new ArrayList(); for (Event event : getService().getAllEvents()) { EventDTO eventDTO = convertToEventDTO(event, false); - eventDTO.setBaseURL(requestBaseURL); + eventDTO.setBaseURL(getEventBaseURLFromEventOrRequest(event)); eventDTO.setIsOnRemoteServer(false); result.add(eventDTO); } @@ -3422,11 +3421,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final com.sap.sse.common.Util.Pair, Exception> eventsOrException = serverRefAndEventsOrException.getValue(); final RemoteSailingServerReference serverRef = serverRefAndEventsOrException.getKey(); final Iterable remoteEvents = eventsOrException.getA(); - String baseURL = getBaseURL(serverRef.getURL()).toString(); + String baseURLFromServerReference = getBaseURL(serverRef.getURL()).toString(); if (remoteEvents != null) { for (EventBase remoteEvent : remoteEvents) { EventBaseDTO remoteEventDTO = convertToEventDTO(remoteEvent); - remoteEventDTO.setBaseURL(baseURL); + remoteEventDTO.setBaseURL(remoteEvent.getBaseURL() == null ? baseURLFromServerReference : remoteEvent.getBaseURL().toString()); remoteEventDTO.setIsOnRemoteServer(true); result.add(remoteEventDTO); } @@ -3481,36 +3480,38 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public EventDTO updateEvent(UUID eventId, String eventName, String eventDescription, Date startDate, Date endDate, - VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURLString, Map sailorsInfoWebsiteURLsByLocaleName, - Iterable images, Iterable videos) throws MalformedURLException { + VenueDTO venue, boolean isPublic, Iterable leaderboardGroupIds, String officialWebsiteURLString, String baseURLAsString, + Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos) throws MalformedURLException { TimePoint startTimePoint = startDate != null ? new MillisecondsTimePoint(startDate) : null; TimePoint endTimePoint = endDate != null ? new MillisecondsTimePoint(endDate) : null; URL officialWebsiteURL = officialWebsiteURLString != null ? new URL(officialWebsiteURLString) : null; + URL baseURL = baseURLAsString != null ? new URL(baseURLAsString) : null; Map sailorsInfoWebsiteURLs = convertToLocalesAndUrls(sailorsInfoWebsiteURLsByLocaleName); List eventImages = convertToImages(images); List eventVideos = convertToVideos(videos); getService().apply( new UpdateEvent(eventId, eventName, eventDescription, startTimePoint, endTimePoint, venue.getName(), - isPublic, leaderboardGroupIds, officialWebsiteURL, sailorsInfoWebsiteURLs, eventImages, eventVideos)); + isPublic, leaderboardGroupIds, officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, eventImages, eventVideos)); return getEventById(eventId, false); } @Override public EventDTO createEvent(String eventName, String eventDescription, Date startDate, Date endDate, String venue, - boolean isPublic, List courseAreaNames, String officialWebsiteURLAsString, Map sailorsInfoWebsiteURLsByLocaleName, - Iterable images, Iterable videos, Iterable leaderboardGroupIds) + boolean isPublic, List courseAreaNames, String officialWebsiteURLAsString, String baseURLAsString, + Map sailorsInfoWebsiteURLsByLocaleName, Iterable images, Iterable videos, Iterable leaderboardGroupIds) throws MalformedURLException { UUID eventUuid = UUID.randomUUID(); TimePoint startTimePoint = startDate != null ? new MillisecondsTimePoint(startDate) : null; TimePoint endTimePoint = endDate != null ? new MillisecondsTimePoint(endDate) : null; URL officialWebsiteURL = officialWebsiteURLAsString != null ? new URL(officialWebsiteURLAsString) : null; + URL baseURL = baseURLAsString != null ? new URL(baseURLAsString) : null; Map sailorsInfoWebsiteURLs = convertToLocalesAndUrls(sailorsInfoWebsiteURLsByLocaleName); List eventImages = convertToImages(images); List eventVideos = convertToVideos(videos); getService().apply( new CreateEvent(eventName, eventDescription, startTimePoint, endTimePoint, venue, isPublic, eventUuid, - officialWebsiteURL, sailorsInfoWebsiteURLs, eventImages, eventVideos, leaderboardGroupIds)); + officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, eventImages, eventVideos, leaderboardGroupIds)); createCourseAreas(eventUuid, courseAreaNames.toArray(new String[courseAreaNames.size()])); return getEventById(eventUuid, false); } @@ -3561,16 +3562,19 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public EventDTO getEventById(UUID id, boolean withStatisticalData) throws MalformedURLException { EventDTO result = null; - String requestBaseURL = getRequestBaseURL().toString(); Event event = getService().getEvent(id); if (event != null) { result = convertToEventDTO(event, withStatisticalData); - result.setBaseURL(requestBaseURL); + result.setBaseURL(getEventBaseURLFromEventOrRequest(event)); result.setIsOnRemoteServer(false); } return result; } + private String getEventBaseURLFromEventOrRequest(Event event) throws MalformedURLException { + return event.getBaseURL() == null ? getRequestBaseURL().toString() : event.getBaseURL().toString(); + } + private EventBaseDTO convertToEventDTO(EventBase event) { final EventBaseDTO eventDTO; if (event == null) { @@ -3603,6 +3607,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S eventDTO.id = (UUID) event.getId(); eventDTO.setDescription(event.getDescription()); eventDTO.setOfficialWebsiteURL(event.getOfficialWebsiteURL() != null ? event.getOfficialWebsiteURL().toString() : null); + eventDTO.setBaseURL(event.getBaseURL() != null ? event.getBaseURL().toString() : null); for(Map.Entry sailorsInfoWebsiteEntry : event.getSailorsInfoWebsiteURLs().entrySet()) { eventDTO.setSailorsInfoWebsiteURL(sailorsInfoWebsiteEntry.getKey() == null ? null : sailorsInfoWebsiteEntry .getKey().toLanguageTag(), sailorsInfoWebsiteEntry.getValue().toExternalForm()); @@ -4364,7 +4369,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } updateEvent(newEvent.id, newEvent.getName(), description, newEvent.startDate, newEvent.endDate, newEvent.venue, newEvent.isPublic, eventLeaderboardGroupUUIDs, newEvent.getOfficialWebsiteURL(), - newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), newEvent.getVideos()); + newEvent.getBaseURL(), newEvent.getSailorsInfoWebsiteURLs(), newEvent.getImages(), newEvent.getVideos()); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java index 9595acc29ea..9daa8e7a97d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java @@ -100,7 +100,7 @@ public class EventBaseDTO extends NamedDTO implements IsSerializable { public void setOfficialWebsiteURL(String officialWebsiteURL) { this.officialWebsiteURL = officialWebsiteURL; } - + public Map getSailorsInfoWebsiteURLs() { return sailorsInfoWebsiteURLs; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html index 575337869d0..4b161402ab6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html @@ -25,6 +25,16 @@ +

  • On the user profile page you now can maintain a list of you favorite boat classes and competitors. + You can also opt in for notification mails about your favorites to stay informed. + A requirement for notification mails to work for you is that you entered a valid mail address + to your user profile and verified the address by clicking the link that has been sent via email. +
  • +
  • Users now may choose their preferred language on the user profile page. + Currently only the mail notification mechanism will accept this setting to send mails in the preferred + language. This also prepares additional language sensitive feature, e.g. automatically switching the + website to your preferred language when logged in. +
  • The leaderboard now has a new measure value called "Ride height", which shows the ride height on foils of a competitor, if respective data is available (currently ESS only). This information can also be shown in the competitor chart on the RaceBaord page. diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java index 8bde462494d..d791f91b93e 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java @@ -132,6 +132,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest final String venueName = "Venue Name"; final String[] courseAreaNames = new String[] { "Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrott" }; final URL officialWebsiteURL = new URL("http://official.website.com"); + final URL baseURL = new URL("http://base.url.com"); final URL sailorsInfoWebsiteURL = new URL("http://sailorsinfo.website.com"); final URL sailorsInfoWebsiteURLDE = new URL("http://sailorsinfo-de.website.com"); final Venue venue = new VenueImpl(venueName); @@ -148,6 +149,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest event.addLeaderboardGroup(lg2); event.setDescription(eventDescription); event.setOfficialWebsiteURL(officialWebsiteURL); + event.setBaseURL(baseURL); event.setSailorsInfoWebsiteURL(null, sailorsInfoWebsiteURL); event.setSailorsInfoWebsiteURL(Locale.GERMAN, sailorsInfoWebsiteURLDE); mof.storeEvent(event); @@ -174,6 +176,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest assertEquals(eventName, loadedEvent.getName()); assertEquals(eventDescription, loadedEvent.getDescription()); assertEquals(event.getOfficialWebsiteURL(), loadedEvent.getOfficialWebsiteURL()); + assertEquals(event.getBaseURL(), loadedEvent.getBaseURL()); assertEquals(sailorsInfoWebsiteURL, loadedEvent.getSailorsInfoWebsiteURL(null)); assertEquals(sailorsInfoWebsiteURLDE, loadedEvent.getSailorsInfoWebsiteURL(Locale.GERMAN)); assertEquals(2, loadedEvent.getSailorsInfoWebsiteURLs().size()); diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/EventBaseJsonDeserializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/EventBaseJsonDeserializer.java index 6a58129445d..d27c9900f5b 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/EventBaseJsonDeserializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/deserialization/impl/EventBaseJsonDeserializer.java @@ -39,6 +39,7 @@ public class EventBaseJsonDeserializer implements JsonDeserializer { String name = (String) eventJson.get(EventBaseJsonSerializer.FIELD_NAME); String description = (String) eventJson.get(EventBaseJsonSerializer.FIELD_DESCRIPTION); String officialWebsiteURLAsString = (String) eventJson.get(EventBaseJsonSerializer.FIELD_OFFICIAL_WEBSITE_URL); + String baseURLAsString = (String) eventJson.get(EventBaseJsonSerializer.FIELD_BASE_URL); Number startDate = (Number) eventJson.get(EventBaseJsonSerializer.FIELD_START_DATE); Number endDate = (Number) eventJson.get(EventBaseJsonSerializer.FIELD_END_DATE); final Venue venue; @@ -65,6 +66,13 @@ public class EventBaseJsonDeserializer implements JsonDeserializer { throw new JsonDeserializationException("Error deserializing official website URL for event "+name, e); } } + if (baseURLAsString != null) { + try { + result.setBaseURL(new URL(baseURLAsString)); + } catch (MalformedURLException e) { + throw new JsonDeserializationException("Error deserializing base URL for event "+name, e); + } + } JSONArray imagesJson = (JSONArray) eventJson.get(EventBaseJsonSerializer.FIELD_IMAGES); if (imagesJson != null) { for (Object imageJson : imagesJson) { diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/EventBaseJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/EventBaseJsonSerializer.java index 1f24f0e7775..63a8b90575a 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/EventBaseJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/EventBaseJsonSerializer.java @@ -27,6 +27,7 @@ public class EventBaseJsonSerializer implements JsonSerializer { public static final String FIELD_IMAGE_HEIGHT = "imageHeight"; public static final String FIELD_VIDEO_URL = "videoURL"; public static final String FIELD_OFFICIAL_WEBSITE_URL = "officialWebsiteURL"; + public static final String FIELD_BASE_URL = "baseURL"; public static final String FIELD_SAILORS_INFO_WEBSITE_URLS = "sailorsInfoWebsiteURLs"; public static final String FIELDS_LEADERBOARD_GROUPS = "leaderboardGroups"; @@ -63,6 +64,7 @@ public class EventBaseJsonSerializer implements JsonSerializer { result.put(FIELD_NAME, event.getName()); result.put(FIELD_DESCRIPTION, event.getDescription()); result.put(FIELD_OFFICIAL_WEBSITE_URL, event.getOfficialWebsiteURL() != null ? event.getOfficialWebsiteURL().toString() : null); + result.put(FIELD_BASE_URL, event.getBaseURL() != null ? event.getBaseURL().toString() : null); result.put(FIELD_START_DATE, event.getStartDate() != null ? event.getStartDate().asMillis() : null); result.put(FIELD_END_DATE, event.getStartDate() != null ? event.getEndDate().asMillis() : null); result.put(FIELD_VENUE, venueSerializer.serialize(event.getVenue())); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerTest.java index 40101a39a72..246f3bd65f4 100755 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerTest.java @@ -48,6 +48,7 @@ public class EventDataJsonSerializerTest { protected final TimePoint expectedEndDate = new MillisecondsTimePoint(new Date()); protected final Venue expectedVenue = new VenueImpl("Expected Venue"); protected final URL expectedOfficialWebsiteURL; + protected final URL expectedBaseURL; protected final Map expectedSailorsInfoWebsiteURLs; protected final URL expectedLogoImageURL; protected final ImageDescriptor expectedLogoImageDescriptor; @@ -60,6 +61,7 @@ public class EventDataJsonSerializerTest { public EventDataJsonSerializerTest() throws MalformedURLException { expectedOfficialWebsiteURL = new URL("http://official.website.com"); + expectedBaseURL = new URL("http://our.own.com"); expectedLogoImageURL = new URL("http://official.logo.com/logo.png"); expectedLogoImageDescriptor = new ImageDescriptorImpl(expectedLogoImageURL, MillisecondsTimePoint.now()); expectedSailorsInfoWebsiteURLs = new HashMap<>(); @@ -76,6 +78,7 @@ public class EventDataJsonSerializerTest { when(event.getName()).thenReturn(expectedName); when(event.getDescription()).thenReturn(expectedDescription); when(event.getOfficialWebsiteURL()).thenReturn(expectedOfficialWebsiteURL); + when(event.getBaseURL()).thenReturn(expectedBaseURL); when(event.getSailorsInfoWebsiteURLs()).thenReturn(expectedSailorsInfoWebsiteURLs); when(event.getSailorsInfoWebsiteURL(null)).thenReturn(expectedSailorsInfoWebsiteURLs.get(null)); when(event.getStartDate()).thenReturn(expectedStartDate); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerWithNullValuesTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerWithNullValuesTest.java index ed382321b68..c69749bfbd7 100755 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerWithNullValuesTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventDataJsonSerializerWithNullValuesTest.java @@ -43,6 +43,7 @@ public class EventDataJsonSerializerWithNullValuesTest { protected final TimePoint expectedEndDate = new MillisecondsTimePoint(new Date()); protected final Venue expectedVenue = new VenueImpl("Expected Venue"); protected final URL expectedOfficialWebsiteURL = null; + protected final URL expectedBaseURL = null; protected final LeaderboardGroup expectedLeaderbaordGroup = mock(LeaderboardGroup.class); protected JsonSerializer venueSerializer; @@ -59,6 +60,7 @@ public class EventDataJsonSerializerWithNullValuesTest { when(event.getName()).thenReturn(expectedName); when(event.getDescription()).thenReturn(expectedDescription); when(event.getOfficialWebsiteURL()).thenReturn(expectedOfficialWebsiteURL); + when(event.getBaseURL()).thenReturn(expectedBaseURL); when(event.getStartDate()).thenReturn(expectedStartDate); when(event.getEndDate()).thenReturn(expectedEndDate); when(event.getVenue()).thenReturn(expectedVenue); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventWithNullStartAndEndDataJsonSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventWithNullStartAndEndDataJsonSerializerTest.java index 0bb26795cf6..b66d29f065b 100755 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventWithNullStartAndEndDataJsonSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/EventWithNullStartAndEndDataJsonSerializerTest.java @@ -46,6 +46,7 @@ public class EventWithNullStartAndEndDataJsonSerializerTest { protected final TimePoint expectedEndDate = null; protected final Venue expectedVenue = new VenueImpl("Expected Venue"); protected final URL expectedOfficialWebsiteURL; + protected final URL expectedBaseURL; protected final URL expectedLogoImageURL; protected final ImageDescriptor expectedLogoImageDescriptor; protected final LeaderboardGroupBase expectedLeaderboardGroup = mock(LeaderboardGroupBase.class); @@ -58,6 +59,7 @@ public class EventWithNullStartAndEndDataJsonSerializerTest { public EventWithNullStartAndEndDataJsonSerializerTest() throws MalformedURLException { expectedOfficialWebsiteURL = new URL("http://official.website.com"); + expectedBaseURL = new URL("http://our.veryown.com"); expectedLogoImageURL = new URL("http://official.logo.com/logo.png"); expectedLogoImageDescriptor = new ImageDescriptorImpl(expectedLogoImageURL, MillisecondsTimePoint.now()); } @@ -74,6 +76,7 @@ public class EventWithNullStartAndEndDataJsonSerializerTest { when(event.getName()).thenReturn(expectedName); when(event.getDescription()).thenReturn(expectedDescription); when(event.getOfficialWebsiteURL()).thenReturn(expectedOfficialWebsiteURL); + when(event.getBaseURL()).thenReturn(expectedBaseURL); when(event.getStartDate()).thenReturn(expectedStartDate); when(event.getEndDate()).thenReturn(expectedEndDate); when(event.getVenue()).thenReturn(expectedVenue); @@ -104,6 +107,7 @@ public class EventWithNullStartAndEndDataJsonSerializerTest { assertEquals(expectedName, deserializedEvent.getName()); assertEquals(expectedDescription, deserializedEvent.getDescription()); assertEquals(expectedOfficialWebsiteURL, deserializedEvent.getOfficialWebsiteURL()); + assertEquals(expectedBaseURL, deserializedEvent.getBaseURL()); assertEquals(1, Util.size(deserializedEvent.getImages())); assertEquals(expectedLogoImageURL, deserializedEvent.getImages().iterator().next().getURL()); LeaderboardGroupBase deserializedLg = deserializedEvent.getLeaderboardGroups().iterator().next(); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/SettingsJsonSerializationTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/SettingsJsonSerializationTest.java deleted file mode 100755 index 58b82e1e8c9..00000000000 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/SettingsJsonSerializationTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.sap.sailing.server.gateway.serialization.test; - -import static org.junit.Assert.assertEquals; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.simple.JSONObject; -import org.junit.Test; - -import com.sap.sse.common.filter.TextOperator; -import com.sap.sse.common.filter.TextOperator.Operators; -import com.sap.sse.common.settings.EnumSetting; -import com.sap.sse.common.settings.ListSetting; -import com.sap.sse.common.settings.MapSettings; -import com.sap.sse.common.settings.NumberSetting; -import com.sap.sse.common.settings.Setting; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.StringSetting; -import com.sap.sse.settings.SettingsToJsonSerializer; - -public class SettingsJsonSerializationTest { - @Test - public void testFlatJsonSerialization() throws ClassNotFoundException { - final Map settingsMap = new HashMap<>(); - settingsMap.put("humba", new StringSetting("trala")); - settingsMap.put("trala", new EnumSetting<>(TextOperator.Operators.Contains)); - settingsMap.put("num", new NumberSetting(BigDecimal.TEN)); - settingsMap.put("l", new ListSetting(Arrays.asList(new NumberSetting(1), new NumberSetting(2), new NumberSetting(3)))); - Settings settings = new MapSettings(settingsMap); - SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); - JSONObject json = serializer.serialize(settings); - Settings deserializedSettings = serializer.deserialize(json); - assertEquals(settingsMap, deserializedSettings.getNonDefaultSettings()); - } - - @Test - public void testNestedJsonSerialization() throws ClassNotFoundException { - final Map settingsMap = new HashMap<>(); - final Map innerMap = new HashMap<>(); - settingsMap.put("humba", new StringSetting("trala")); - innerMap.put("num", new NumberSetting(123.4)); - settingsMap.put("map", new MapSettings(innerMap)); - Settings settings = new MapSettings(settingsMap); - SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); - JSONObject json = serializer.serialize(settings); - Settings deserializedSettings = serializer.deserialize(json); - assertEquals(settingsMap, deserializedSettings.getNonDefaultSettings()); - } - - /** - * A white-box test that tests property name escaping in case the property name conflicts with how type - * names (used particularly for enum settings) are represented in JSON. - */ - @Test - public void testJsonSerializationWithFunnyPropertyName() throws ClassNotFoundException { - final Map settingsMap = new HashMap<>(); - settingsMap.put("humba___TYPE", new StringSetting("trala")); - settingsMap.put("trala___TYPE___TYPE", new EnumSetting<>(TextOperator.Operators.Contains)); - Settings settings = new MapSettings(settingsMap); - SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); - JSONObject json = serializer.serialize(settings); - Settings deserializedSettings = serializer.deserialize(json); - assertEquals(settingsMap, deserializedSettings.getNonDefaultSettings()); - } - - /** - * A white-box test that tests property name escaping in case the property name conflicts with how type - * names (used particularly for enum settings) are represented in JSON. - */ - @Test - public void testJsonSerializationWithEnumTypedList() throws ClassNotFoundException { - final Map settingsMap = new HashMap<>(); - final List> list = new ArrayList<>(); - list.add(new EnumSetting(Operators.Contains)); - list.add(new EnumSetting(Operators.EndsWith)); - settingsMap.put("l", new ListSetting>(list)); - Settings settings = new MapSettings(settingsMap); - SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); - JSONObject json = serializer.serialize(settings); - Settings deserializedSettings = serializer.deserialize(json); - assertEquals(settingsMap, deserializedSettings.getNonDefaultSettings()); - } - - /** - * A white-box test that tests property name escaping in case the property name conflicts with how type - * names (used particularly for enum settings) are represented in JSON. - */ - @Test - public void testJsonSerializationWithStringifiedEnum() throws ClassNotFoundException { - final Map settingsMap = new HashMap<>(); - final List list = new ArrayList<>(); - list.add(new StringSetting(Operators.Contains)); - list.add(new StringSetting(Operators.EndsWith)); - settingsMap.put("l", new ListSetting(list)); - Settings settings = new MapSettings(settingsMap); - SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); - JSONObject json = serializer.serialize(settings); - Settings deserializedSettings = serializer.deserialize(json); - ListSetting l = (ListSetting) deserializedSettings.getNonDefaultSettings().get("l"); - assertEquals(Arrays.asList(Operators.Contains, Operators.EndsWith), l.getEnumList(Operators.class)); - } - -} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java index fb3f944344f..5d3f8db5a36 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/trackfiles/impl/SensorDataImportServlet.java @@ -52,8 +52,6 @@ public class SensorDataImportServlet extends AbstractFileUploadServlet { * Searches the requested importer in the importers provided by the OSGi registry and imports the priovided sensor * data file. * - * @param files - * @return * @throws IOException */ private Iterable importFiles(Iterable> files) diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java index 269a75ebe99..8a729da4acf 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java @@ -55,7 +55,7 @@ public abstract class AbstractServerReplicationTest extends com.sap.sse.replicat @Override public DomainFactory getBaseDomainFactory() { return baseDomainFactory; } @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; - }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, null, null); + }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, null, null, /* sailingNotificationService */ null); } @Override @@ -74,7 +74,7 @@ public abstract class AbstractServerReplicationTest extends com.sap.sse.replicat @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, - /* serviceFinderFactory */ null, null); + /* serviceFinderFactory */ null, null, /* sailingNotificationService */ null); } } } diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java index 3993c0fc5de..915604c2c40 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java @@ -1369,9 +1369,9 @@ public class MasterDataImportTest { Event event = sourceService.createEventWithoutReplication("Test Event", /* eventDescription */null, new MillisecondsTimePoint(0), new MillisecondsTimePoint(10), "testvenue", false, UUID.randomUUID(), - /* officialWebsiteURL */null, /* sailorsInfoWebsiteURL */null, - /* images */Collections. emptyList(), /* videos */ - Collections. emptyList()); + /* officialWebsiteURL */null, /*baseURL*/null, + /* sailorsInfoWebsiteURL */null, /* videos */ + /* images */Collections. emptyList(), Collections. emptyList()); CourseArea defaultCourseArea = sourceService.addCourseAreas(event.getId(), new String[] { "ECHO" }, new UUID[] { UUID.randomUUID() })[0]; @@ -1437,9 +1437,9 @@ public class MasterDataImportTest { Event event = sourceService.createEventWithoutReplication("Test Event", /* eventDescription */null, new MillisecondsTimePoint(0), new MillisecondsTimePoint(10), "testvenue", false, UUID.randomUUID(), - /* officialWebsiteURL */null, /* sailorsInfoWebsiteURL */null, - /* images */Collections. emptyList(), /* videos */ - Collections. emptyList()); + /* officialWebsiteURL */null, /*baseURL*/null, + /* sailorsInfoWebsiteURL */null, /* videos */ + /* images */Collections. emptyList(), Collections. emptyList()); CourseArea defaultCourseArea = sourceService.addCourseAreas(event.getId(), new String[] { "ECHO" }, new UUID[] { UUID.randomUUID() })[0]; diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RemoveLeaderboardGroupTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RemoveLeaderboardGroupTest.java index 482d86be248..16ecd2cc100 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RemoveLeaderboardGroupTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/RemoveLeaderboardGroupTest.java @@ -68,9 +68,9 @@ public class RemoveLeaderboardGroupTest { cal.set(2014, 5, 8, 16, 00); final TimePoint pfingstbuschEndDate = new MillisecondsTimePoint(cal.getTime()); pfingstbusch = server.apply(new CreateEvent("Pfingstbusch", /* eventDescription */ null, pfingstbuschStartDate, pfingstbuschEndDate, - "Kiel", /* isPublic */ true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /* sailorsInfoWebsiteURLAsString */ null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList(), - /* leaderboardGroupIds */ Collections. emptyList())); + "Kiel", /* isPublic */ true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /*baseURL*/null, + /* sailorsInfoWebsiteURLAsString */ null, /* images */Collections. emptyList(), + /* videos */Collections. emptyList(), /* leaderboardGroupIds */ Collections. emptyList())); final LeaderboardGroup pfingstbuschLeaderboardGroup = server.apply(new CreateLeaderboardGroup("Pfingstbusch", "Pfingstbusch", /* displayName */ null, /* displayGroupsInReverseOrder */ false, /* leaderboard names */ Collections.emptyList(), new int[0], /* overallLeaderboardScoringSchemeType */ ScoringSchemeType.LOW_POINT)); diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java index 26ac1ad609f..8344d056a99 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/SearchServiceTest.java @@ -126,8 +126,8 @@ public class SearchServiceTest { cal.set(2014, 5, 8, 16, 00); final TimePoint pfingstbuschEndDate = new MillisecondsTimePoint(cal.getTime()); pfingstbusch = server.apply(new CreateEvent("Pfingsbusch", /* eventDescription */ null, pfingstbuschStartDate, pfingstbuschEndDate, - "Kiel", /* isPublic */ true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /* sailorsInfoWebsiteURLAsString */ null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList(), /* leaderboardGroupIds */ Collections. emptyList())); + "Kiel", /* isPublic */ true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /*baseURL*/null, + /* sailorsInfoWebsiteURLAsString */ null, /* images */Collections. emptyList(), /* videos */Collections. emptyList(), /* leaderboardGroupIds */ Collections. emptyList())); kiel = pfingstbusch.getVenue(); final CourseAreaImpl kielAlpha = new CourseAreaImpl("Alpha", UUID.randomUUID()); kiel.addCourseArea(kielAlpha); @@ -164,8 +164,8 @@ public class SearchServiceTest { cal.set(2014, 5, 8, 18, 00); final TimePoint aalEndDate = new MillisecondsTimePoint(cal.getTime()); aalEvent = server.apply(new CreateEvent("Aalregatta", /* eventDescription */ null, aalStartDate, aalEndDate, - "Flensburg", /* isPublic */ true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /*sailorsInfoWebsiteURLAsString */ null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList(), /* leaderboardGroupIds */ Collections. emptyList())); + "Flensburg", /* isPublic */ true, UUID.randomUUID(), /* officialWebsiteURLAsString */ null, /*baseURL*/null, + /*sailorsInfoWebsiteURLAsString */ null, /* images */Collections. emptyList(), /* videos */Collections. emptyList(), /* leaderboardGroupIds */ Collections. emptyList())); flensburg = aalEvent.getVenue(); final CourseAreaImpl flensburgStandard = new CourseAreaImpl("Standard", UUID.randomUUID()); flensburg.addCourseArea(flensburgStandard); diff --git a/java/com.sap.sailing.server/.classpath b/java/com.sap.sailing.server/.classpath index 4f83b2397ec..d47cca9a1e3 100644 --- a/java/com.sap.sailing.server/.classpath +++ b/java/com.sap.sailing.server/.classpath @@ -1,7 +1,8 @@ - - - - - - - + + + + + + + + diff --git a/java/com.sap.sailing.server/META-INF/MANIFEST.MF b/java/com.sap.sailing.server/META-INF/MANIFEST.MF index ca1e4920472..29807ffc25f 100644 --- a/java/com.sap.sailing.server/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server/META-INF/MANIFEST.MF @@ -7,6 +7,9 @@ Bundle-Activator: com.sap.sailing.server.impl.Activator Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: com.sap.sailing.server.gateway.deserialization, + javax.mail;version="1.4.0", + javax.mail.internet;version="1.4.0", + javax.mail.util;version="1.4.0", org.json.simple, org.json.simple.parser, org.osgi.framework;version="1.3.0", @@ -33,7 +36,8 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sse.security, org.apache.shiro.web;bundle-version="1.2.2", org.apache.shiro.core;bundle-version="1.2.2", - javax.servlet;bundle-version="3.1.0" + javax.servlet;bundle-version="3.1.0", + com.sap.sse.mail Bundle-ClassPath: . Export-Package: com.sap.sailing.server, com.sap.sailing.server.impl; @@ -45,6 +49,7 @@ Export-Package: com.sap.sailing.server, com.sap.sailing.server.replication.test, com.sap.sailing.domain.racelogtrackingadapter.test, com.sap.sailing.server.testsupport", + com.sap.sailing.server.impl.preferences.model, com.sap.sailing.server.masterdata, com.sap.sailing.server.operationaltransformation, com.sap.sailing.server.operationaltransformation.racelog, diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch index 236f0b4e8b4..c902bdcac5c 100755 --- a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch @@ -17,7 +17,7 @@ - + diff --git a/java/com.sap.sailing.server/build.properties b/java/com.sap.sailing.server/build.properties index 41eb6ade2b4..a2e16dfebcd 100755 --- a/java/com.sap.sailing.server/build.properties +++ b/java/com.sap.sailing.server/build.properties @@ -1,4 +1,5 @@ -source.. = src/ +source.. = src/,\ + resources/ output.. = bin/ bin.includes = META-INF/,\ . diff --git a/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/notification-mail-template.html b/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/notification-mail-template.html new file mode 100644 index 00000000000..c9dba3e0a9d --- /dev/null +++ b/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/notification-mail-template.html @@ -0,0 +1,30 @@ + + + + ${title} + + + + + +
    +
    +
    + Sailing Analytics +
    +
    + ${content} +
    +
    + +
    + + \ No newline at end of file diff --git a/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/notification-mail-template.original.html b/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/notification-mail-template.original.html new file mode 100644 index 00000000000..3747718bfac --- /dev/null +++ b/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/notification-mail-template.original.html @@ -0,0 +1,100 @@ + + + + + ${title} + + + + +
    +
    +
    + Sailing Analytics +
    +
    + ${content} +
    +
    + +
    + + \ No newline at end of file diff --git a/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/sap_logo_header.png b/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/sap_logo_header.png new file mode 100644 index 00000000000..cac9fd66d07 Binary files /dev/null and b/java/com.sap.sailing.server/resources/com/sap/sailing/server/notification/impl/sap_logo_header.png differ diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties new file mode 100644 index 00000000000..f2963fcde53 --- /dev/null +++ b/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties @@ -0,0 +1,34 @@ +raceInRegattaOfEvent=race {0} in regatta '{1}' of event '{2}' +raceInFleetInRegattaOfEvent=race {0} ({3}) in regatta '{1}' of event '{2}' +leaderboardOfEvent=leaderboard '{0}' of event '{1}' + +leaderboardShowResultsLinkTitle=Show results +racesOverviewLinkTitle=Show races overview +raceboardShowRaceLinkTitle=Show race +raceboardRaceAnalysisLinkTitle=Show race analysis + +boatClassRaceFinishedSubject=Race for boat class {0} has finished - new results available +# 0: boatClassName, 1: raceDescription +boatClassRaceFinishedBody=New results for boat class {0} in {1} are available. + +boatClassScoreCorrectionSubject=New results are available for boat class {0} +# 0: boatClassName, 1: leaderboardDescription +boatClassScoreCorrectionBody=New results for boat class {0} in {1} are available. + +competitorPassesFinishSubject=Competitor {0} passed finish line in a race +# 0: competitorName, 1: raceDescription +competitorPassesFinishBody=Competitor {0} passed finish line in {1}. + +competitorScoreCorrectionSubject=New results are available for competitor {0} +# 0: competitorName, 1: leaderboardDescription +competitorScoreCorrectionBody=New results for competitor {0} in {1} are available. + +boatClassUpcomingRaceSubject=A race for boat class {0} will start soon +# 0: boatClassName, 1: raceDescription, 2: time +boatClassUpcomingRaceBody=For boat class {0} {1} will start approximately at {2}. + +subscriptionInformation=You are receiving notifications from SAP Sailing Analytics. To turn off notifications, please visit your user profile at the link below. +userProfile=User Profile +imprint=Imprint +privacy=Privacy +salutation=Hallo {0}, diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages_de.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages_de.properties new file mode 100644 index 00000000000..f287ccc8493 --- /dev/null +++ b/java/com.sap.sailing.server/resources/stringmessages/StringMessages_de.properties @@ -0,0 +1,34 @@ +raceInRegattaOfEvent=Wettfahrt {0} der Regatta "{1}" bei Veranstaltung "{2}" +raceInFleetInRegattaOfEvent=Wettfahrt {0} ({3}) der Regatta "{1}" bei Veranstaltung "{2}" +leaderboardOfEvent=Rangliste "{0}" bei Veranstaltung "{1}" + +leaderboardShowResultsLinkTitle=Ergebnisse anzeigen +racesOverviewLinkTitle=Wettfahrtübersicht anzeigen +raceboardShowRaceLinkTitle=Wettfahrt anzeigen +raceboardRaceAnalysisLinkTitle=Wettfahrtanalyse anzeigen + +boatClassRaceFinishedSubject=Wettfahrt für Bootsklasse {0} beendet - Neue Ergebnisse verfügbar +# 0: boatClassName, 1: raceDescription +boatClassRaceFinishedBody=Neue Ergebnisse für Bootsklasse {0} in {1} sind verfügbar. + +boatClassScoreCorrectionSubject=Neue Ergebnisse für Bootsklasse {0} verfügbar +# 0: boatClassName, 1: leaderboardDescription +boatClassScoreCorrectionBody=Neue Ergebnisse für Bootsklasse {0} in {1} sind verfügbar. + +competitorPassesFinishSubject=Teilnehmer {0} hat eine Wettfahrt beendet +# 0: competitorName, 1: raceDescription +competitorPassesFinishBody=Teilnehmer {0} hat bei {1} die Ziellinie überquert. + +competitorScoreCorrectionSubject=Neue Ergebnisse für Teilnehmer {0} +# 0: competitorName, 1: leaderboardDescription +competitorScoreCorrectionBody=Für Teilnehmer {0} in {1} sind neue Ergebnisse verfügbar. + +boatClassUpcomingRaceSubject=Wettfahrt in Bootsklasse {0} startet bald +# 0: boatClassName, 1: raceDescription, 2: time +boatClassUpcomingRaceBody=In Bootsklasse {0} startet {1} um etwa {2}. + +subscriptionInformation=Bei dieser Mail handelt es sich um eine automatisierte Benachrichtigung von SAP Sailing Analytics. Die Benachrichtigungen können im Benutzerprofil (s.u.) abgeschaltet werden. +userProfile=Benutzerprofil +imprint=Impressum +privacy=Datenschutzerklärung +salutation=Hallo {0}, diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages_ja.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages_ja.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages_ru.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages_ru.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages_zh.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages_zh.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages_zh_CN.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages_zh_CN.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index d28ea8c2a6c..f300a5348d3 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -369,11 +369,12 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch * The name of the venue of the event * @param isPublic * Indicates whether the event is public accessible via the publication URL or not + * @param baseURL TODO * @return The new event */ void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, - String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, Map sailorsInfoWebsiteURLs, - Iterable images, Iterable videos); + String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL baseURL, + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos); /** * Renames a sailing event. If a sailing event by the name oldName does not exist in {@link #getEvents()}, @@ -468,8 +469,8 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch ConcurrentHashMap getPersistentRegattasForRaceIDs(); Event createEventWithoutReplication(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venue, - boolean isPublic, UUID id, URL officialWebsiteURL, Map sailorsInfoWebsiteURLs, Iterable images, - Iterable videos); + boolean isPublic, UUID id, URL officialWebsiteURL, URL baseURL, Map sailorsInfoWebsiteURLs, + Iterable images, Iterable videos); void setRegattaForRace(Regatta regatta, String raceIdAsString); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java index 56ab8037e7c..62ba19f87c4 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java @@ -32,60 +32,82 @@ import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.server.MasterDataImportClassLoaderService; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.RacingEventServiceMXBean; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreferences; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences; +import com.sap.sailing.server.notification.impl.SailingNotificationServiceImpl; import com.sap.sse.common.TypeBasedServiceFinder; import com.sap.sse.common.Util; +import com.sap.sse.mail.MailService; +import com.sap.sse.mail.queue.MailQueue; +import com.sap.sse.mail.queue.impl.ExecutorMailQueue; import com.sap.sse.osgi.CachedOsgiTypeBasedServiceFinderFactory; import com.sap.sse.replication.Replicable; +import com.sap.sse.security.PreferenceConverter; import com.sap.sse.util.ClearStateTestSupport; +import com.sap.sse.util.ServiceTrackerFactory; public class Activator implements BundleActivator { private static final Logger logger = Logger.getLogger(Activator.class.getName()); private static final String CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME = "persistentcompetitors.clear"; - + private static ExtenderBundleTracker extenderBundleTracker; - + private CachedOsgiTypeBasedServiceFinderFactory serviceFinderFactory; private RacingEventServiceImpl racingEventService; - + private final boolean clearPersistentCompetitors; - + private Set> registrations = new HashSet<>(); private ObjectName mBeanName; - + private ServiceTracker masterDataImportClassLoaderServiceTracker; - + private ServiceTracker polarDataServiceTracker; private OSGiBasedTrackedRegattaListener trackedRegattaListener; - + + private MailQueue mailQueue; + + private SailingNotificationServiceImpl notificationService; + + private ServiceTracker mailServiceTracker; + public Activator() { - clearPersistentCompetitors = Boolean.valueOf(System.getProperty(CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME, ""+false)); - logger.log(Level.INFO, "setting "+CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME+" to "+clearPersistentCompetitors); + clearPersistentCompetitors = Boolean + .valueOf(System.getProperty(CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME, "" + false)); + logger.log(Level.INFO, + "setting " + CLEAR_PERSISTENT_COMPETITORS_PROPERTY_NAME + " to " + clearPersistentCompetitors); // there is exactly one instance of the racingEventService in the whole server } - + public void start(BundleContext context) throws Exception { extenderBundleTracker = new ExtenderBundleTracker(context); extenderBundleTracker.open(); - + + mailServiceTracker = ServiceTrackerFactory.createAndOpen(context, MailService.class); + mailQueue = new ExecutorMailQueue(mailServiceTracker); + notificationService = new SailingNotificationServiceImpl(context, mailQueue); + trackedRegattaListener = new OSGiBasedTrackedRegattaListener(context); - + // At this point the OSGi resolver is used as device type service finder. // In the case that we are not in an OSGi context (e.g. running a JUnit test instead), // this code block is not run, and the test case can inject some other type of finder // instead. serviceFinderFactory = new CachedOsgiTypeBasedServiceFinderFactory(context); - racingEventService = new RacingEventServiceImpl(clearPersistentCompetitors, serviceFinderFactory, trackedRegattaListener); - + racingEventService = new RacingEventServiceImpl(clearPersistentCompetitors, serviceFinderFactory, + trackedRegattaListener, notificationService); + notificationService.setRacingEventService(racingEventService); + masterDataImportClassLoaderServiceTracker = new ServiceTracker( context, MasterDataImportClassLoaderService.class, new MasterDataImportClassLoaderServiceTrackerCustomizer(context, racingEventService)); masterDataImportClassLoaderServiceTracker.open(); - + polarDataServiceTracker = new ServiceTracker(context, PolarDataService.class, new PolarDataServiceTrackerCustomizer(context, racingEventService)); polarDataServiceTracker.open(); @@ -93,40 +115,54 @@ public class Activator implements BundleActivator { // register the racing service in the OSGi registry racingEventService.setBundleContext(context); final Dictionary replicableServiceProperties = new Hashtable<>(); - replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name, racingEventService.getId().toString()); + replicableServiceProperties.put(Replicable.OSGi_Service_Registry_ID_Property_Name, + racingEventService.getId().toString()); context.registerService(Replicable.class.getName(), racingEventService, replicableServiceProperties); context.registerService(RacingEventService.class.getName(), racingEventService, null); context.registerService(ClearStateTestSupport.class.getName(), racingEventService, null); Dictionary properties = new Hashtable(); - final GPSFixMongoHandlerImpl gpsFixMongoHandler = new GPSFixMongoHandlerImpl(racingEventService.getMongoObjectFactory(), - racingEventService.getDomainObjectFactory()); + final GPSFixMongoHandlerImpl gpsFixMongoHandler = new GPSFixMongoHandlerImpl( + racingEventService.getMongoObjectFactory(), racingEventService.getDomainObjectFactory()); properties.put(TypeBasedServiceFinder.TYPE, GPSFixImpl.class.getName()); - registrations.add(context.registerService(FixMongoHandler.class, - gpsFixMongoHandler, properties)); + registrations.add(context.registerService(FixMongoHandler.class, gpsFixMongoHandler, properties)); // legacy type name; some DBs may still contain fixes marked with this old package name: properties.put(TypeBasedServiceFinder.TYPE, "com.sap.sailing.domain.tracking.impl.GPSFixImpl"); - registrations.add(context.registerService(FixMongoHandler.class, - gpsFixMongoHandler, properties)); - final GPSFixMovingMongoHandlerImpl gpsFixMovingMongoHandler = new GPSFixMovingMongoHandlerImpl(racingEventService.getMongoObjectFactory(), - racingEventService.getDomainObjectFactory()); + registrations.add(context.registerService(FixMongoHandler.class, gpsFixMongoHandler, properties)); + final GPSFixMovingMongoHandlerImpl gpsFixMovingMongoHandler = new GPSFixMovingMongoHandlerImpl( + racingEventService.getMongoObjectFactory(), racingEventService.getDomainObjectFactory()); properties.put(TypeBasedServiceFinder.TYPE, GPSFixMovingImpl.class.getName()); registrations.add(context.registerService(FixMongoHandler.class, gpsFixMovingMongoHandler, properties)); // legacy type name; some DBs may still contain fixes marked with this old package name: properties.put(TypeBasedServiceFinder.TYPE, "com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl"); registrations.add(context.registerService(FixMongoHandler.class, gpsFixMovingMongoHandler, properties)); properties.put(TypeBasedServiceFinder.TYPE, DoubleVectorFixImpl.class.getName()); - registrations.add(context.registerService(FixMongoHandler.class, new DoubleVectorFixMongoHandlerImpl(racingEventService.getMongoObjectFactory(), - racingEventService.getDomainObjectFactory()), properties)); - + registrations.add(context.registerService(FixMongoHandler.class, new DoubleVectorFixMongoHandlerImpl( + racingEventService.getMongoObjectFactory(), racingEventService.getDomainObjectFactory()), properties)); + registerPreferenceConvertersForUserStore(context); // Add an MBean for the service to the JMX bean server: RacingEventServiceMXBean mbean = new RacingEventServiceMXBeanImpl(racingEventService); MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); mBeanName = new ObjectName("com.sap.sailing:type=RacingEventService"); mbs.registerMBean(mbean, mBeanName); - logger.log(Level.INFO, "Started "+context.getBundle().getSymbolicName()+". Character encoding: "+ - Charset.defaultCharset()); + logger.log(Level.INFO, "Started " + context.getBundle().getSymbolicName() + ". Character encoding: " + + Charset.defaultCharset()); } - + + /** + * {@link #racingEventService} must already be initialized when calling this method + */ + protected void registerPreferenceConvertersForUserStore(BundleContext context) { + Dictionary properties = new Hashtable(); + properties.put(PreferenceConverter.KEY_PARAMETER_NAME, CompetitorNotificationPreferences.PREF_NAME); + registrations.add(context.registerService(PreferenceConverter.class, + new GenericJSONPreferenceConverter<>(() -> new CompetitorNotificationPreferences(racingEventService)), + properties)); + properties.put(PreferenceConverter.KEY_PARAMETER_NAME, BoatClassNotificationPreferences.PREF_NAME); + registrations.add(context.registerService(PreferenceConverter.class, + new GenericJSONPreferenceConverter<>(() -> new BoatClassNotificationPreferences(racingEventService)), + properties)); + } + public void stop(BundleContext context) throws Exception { masterDataImportClassLoaderServiceTracker.close(); if (extenderBundleTracker != null) { @@ -147,17 +183,21 @@ public class Activator implements BundleActivator { } trackedRegattaListener.close(); registrations.clear(); + notificationService.stop(); + mailQueue.stop(); + mailServiceTracker.close(); MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); mbs.unregisterMBean(mBeanName); } - + private class MasterDataImportClassLoaderServiceTrackerCustomizer implements ServiceTrackerCustomizer { private final BundleContext context; private RacingEventServiceImpl racingEventService; - public MasterDataImportClassLoaderServiceTrackerCustomizer(BundleContext context, RacingEventServiceImpl racingEventService) { + public MasterDataImportClassLoaderServiceTrackerCustomizer(BundleContext context, + RacingEventServiceImpl racingEventService) { this.context = context; this.racingEventService = racingEventService; } @@ -172,7 +212,8 @@ public class Activator implements BundleActivator { @Override public void modifiedService(ServiceReference reference, - MasterDataImportClassLoaderService service) {} + MasterDataImportClassLoaderService service) { + } @Override public void removedService(ServiceReference reference, @@ -182,8 +223,8 @@ public class Activator implements BundleActivator { } - private class PolarDataServiceTrackerCustomizer implements - ServiceTrackerCustomizer { + private class PolarDataServiceTrackerCustomizer + implements ServiceTrackerCustomizer { private final BundleContext context; private RacingEventServiceImpl racingEventService; @@ -210,5 +251,5 @@ public class Activator implements BundleActivator { } } - + } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/GenericJSONPreferenceConverter.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/GenericJSONPreferenceConverter.java new file mode 100644 index 00000000000..383e61430c5 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/GenericJSONPreferenceConverter.java @@ -0,0 +1,28 @@ +package com.sap.sailing.server.impl; + +import java.util.function.Supplier; + +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.security.PreferenceConverter; +import com.sap.sse.shared.settings.SettingsToJsonSerializer; + +public class GenericJSONPreferenceConverter implements PreferenceConverter { + + private final SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); + private final Supplier emptyInstanceFactory; + + public GenericJSONPreferenceConverter(Supplier emptyInstanceFactory) { + this.emptyInstanceFactory = emptyInstanceFactory; + } + + @Override + public String toPreferenceString(PREF preference) { + return serializer.serializeToString(preference); + } + + @Override + public PREF toPreferenceObject(String stringPreference) { + return serializer.deserialize(emptyInstanceFactory.get(), stringPreference); + } + +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicatorAndNotifier.java similarity index 82% rename from java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java rename to java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicatorAndNotifier.java index df2e1f6b302..a231a79a8a5 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicatorAndNotifier.java @@ -18,21 +18,28 @@ import com.sap.sailing.server.operationaltransformation.RecordRaceLogEventOnLead import com.sap.sailing.server.operationaltransformation.RecordRaceLogEventOnRegatta; /** - * Being a {@link RaceColumnListener}, this replicator must be added to all {@link RaceColumn}s managing a + * Being a {@link RaceColumnListener}, this replicator and notifier must be added to all {@link RaceColumn}s managing a * {@link RaceLog} so as to be notified about changes to the race log. This largely happens by callbacks to the * {@link #raceLogEventAdded(RaceColumn, RaceLogIdentifier, RaceLogEvent)} method. This object will then use the * {@link Replicator} passed to this object's constructor and send a {@link RecordRaceLogEventOnLeaderboard} or a - * {@link RecordRaceLogEventOnRegatta} operation to all replicas. + * {@link RecordRaceLogEventOnRegatta} operation to all replicas.

    + * + * Since all {@link RaceColumn}s can manage a {@link RaceLog}, this object is registered on all relevant + * {@link RaceColumn}s. This also lets this object conveniently being informed about all other events that + * a {@link RaceColumnListener} is notified about, some of which contain interesting information regarding + * the notifying of users interested in certain types of changes. For example, when a {@link TrackedRace} + * {@link #trackedRaceLinked(RaceColumn, Fleet, TrackedRace) is linked} to a race column, this may be of + * interest to some users. * * @author Axel Uhl (d043530) * */ -public class RaceLogReplicator implements RaceColumnListener { +public class RaceLogReplicatorAndNotifier implements RaceColumnListener { private static final long serialVersionUID = 7190510926643574068L; private final Replicator service; - public RaceLogReplicator(Replicator service) { + public RaceLogReplicatorAndNotifier(Replicator service) { this.service = service; } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 5604dc5d538..a1a22d5d16b 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -95,6 +95,7 @@ import com.sap.sailing.domain.base.impl.RemoteSailingServerReferenceImpl; import com.sap.sailing.domain.common.DataImportProgress; import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaIdentifier; @@ -119,7 +120,9 @@ import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.LeaderboardGroup; import com.sap.sailing.domain.leaderboard.LeaderboardRegistry; import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ScoreCorrectionListener; import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl; import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl; @@ -177,6 +180,8 @@ import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJ import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResultBaseJsonDeserializer; import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer; import com.sap.sailing.server.masterdata.DataImportLockWithProgress; +import com.sap.sailing.server.notification.EmptySailingNotificationService; +import com.sap.sailing.server.notification.SailingNotificationService; import com.sap.sailing.server.operationaltransformation.AddCourseAreas; import com.sap.sailing.server.operationaltransformation.AddDefaultRegatta; import com.sap.sailing.server.operationaltransformation.AddMediaTrackOperation; @@ -220,6 +225,7 @@ import com.sap.sailing.server.operationaltransformation.UpdateWindSourcesToExclu import com.sap.sailing.server.simulation.SimulationService; import com.sap.sailing.server.simulation.SimulationServiceFactory; import com.sap.sse.ServerInfo; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TypeBasedServiceFinderFactory; import com.sap.sse.common.Util; @@ -362,7 +368,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes */ private final ConcurrentHashMap persistentRegattasForRaceIDs; - private final RaceLogReplicator raceLogReplicator; + private final RaceLogReplicatorAndNotifier raceLogReplicator; private final RegattaLogReplicator regattaLogReplicator; private final RaceLogScoringReplicator raceLogScoringReplicator; @@ -390,6 +396,18 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private final SimulationService simulationService; + /** + * A service that, if not {@code null}, must be called when certain events that the service can notify users about + * have occurred. For example, the + * {@link SailingNotificationService#notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(com.sap.sailing.domain.base.BoatClass, Leaderboard)} + * method must be called whenever a new set of score corrections have been made available.

    + * + * The field is transient because we don't want the service to be serialized from a master to a replica. + * Replicas are expected to not notify users about anything, particularly because they usually don't + * have a valid mail service, either. + */ + private transient SailingNotificationService notificationService; + /** * Allow only one master data import at a time to avoid situation where multiple Imports override each other in * unpredictable fashion @@ -422,6 +440,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private SailingServerConfiguration sailingServerConfiguration; private final TrackedRegattaListener trackedRegattaListener; + + private transient ConcurrentHashMap scoreCorrectionListenersByLeaderboard; /** * Providing the constructor parameters for a new {@link RacingEventServiceImpl} instance is a bit tricky @@ -453,7 +473,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes public RacingEventServiceImpl(WindStore windStore, SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory) { - this(true, windStore, sensorFixStore, serviceFinderFactory); + this(true, windStore, sensorFixStore, serviceFinderFactory, /* sailingNotificationService */ null); } void setBundleContext(BundleContext bundleContext) { @@ -461,7 +481,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory) { - this(clearPersistentCompetitorStore, serviceFinderFactory, null); + this(clearPersistentCompetitorStore, serviceFinderFactory, null, /* sailingNotificationService */ null); } /** @@ -472,8 +492,12 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * if true, the {@link PersistentCompetitorStore} is created empty, with the corresponding * database collection cleared as well. Use with caution! When used with false, competitors * created and stored during previous service executions will initially be loaded. + * @param sailingNotificationService + * a notification service to call upon events worth notifying users about, or {@code null} if no + * notification service is available, e.g., in test set-ups */ - public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory, TrackedRegattaListener trackedRegattaListener) { + public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory, + TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService) { this((final RaceLogResolver raceLogResolver)-> { return new ConstructorParameters() { private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory); @@ -486,11 +510,11 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); } @Override public CompetitorStore getCompetitorStore() { return competitorStore; } }; - }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory ,trackedRegattaListener); + }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory ,trackedRegattaListener, sailingNotificationService); } private RacingEventServiceImpl(final boolean clearPersistentCompetitorStore, WindStore windStore, - SensorFixStore sensorFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory) { + SensorFixStore sensorFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory, SailingNotificationService sailingNotificationService) { this((final RaceLogResolver raceLogResolver)-> { return new ConstructorParameters() { private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory); @@ -503,29 +527,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); } @Override public CompetitorStore getCompetitorStore() { return competitorStore; } }; - }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, sensorFixStore, serviceFinderFactory, null); + }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, sensorFixStore, serviceFinderFactory, null, sailingNotificationService); } - /** - * Uses the default factories for the tracking adapters and the {@link DomainFactory base domain factory} of the - * {@link PersistenceFactory#getDefaultDomainObjectFactory() default domain object factory}. This constructor should - * be used for testing because it provides a transient {@link CompetitorStore} as required for competitor - * persistence. - */ - public RacingEventServiceImpl(Function domainObjectFactoryProvider, - final MongoObjectFactory mongoObjectFactory, MediaDB mediaDB, WindStore windStore, SensorFixStore sensorFixStore) { - this((final RaceLogResolver raceLogResolver)-> { - return new ConstructorParameters() { - private final DomainObjectFactory domainObjectFactory = domainObjectFactoryProvider.apply(raceLogResolver); - - @Override public DomainObjectFactory getDomainObjectFactory() { return domainObjectFactory; } - @Override public MongoObjectFactory getMongoObjectFactory() { return mongoObjectFactory; } - @Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); } - @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } - }; - }, mediaDB, windStore, sensorFixStore, null, null); - } - public RacingEventServiceImpl(final DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory, MediaDB mediaDB, WindStore windStore, SensorFixStore sensorFixStore) { this((final RaceLogResolver raceLogResolver)-> { @@ -535,7 +539,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); } @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; - }, mediaDB, windStore, sensorFixStore, null, null); + }, mediaDB, windStore, sensorFixStore, null, null, /* sailingNotificationService */ null); } /** @@ -546,10 +550,16 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * used to find the services handling specific types of tracking devices, such as the persistent storage * of {@link DeviceIdentifier}s of specific device types or the managing of the device-to-competitor * associations per race tracked. + * @param sailingNotificationService + * a notification service to call upon events worth notifying users about, or {@code null} if no + * notification service is available, e.g., in test set-ups */ public RacingEventServiceImpl(Function constructorParametersProvider, MediaDB mediaDb, - final WindStore windStore, final SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory, TrackedRegattaListener trackedRegattaListener) { + final WindStore windStore, final SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory, + TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService) { logger.info("Created " + this); + this.scoreCorrectionListenersByLeaderboard = new ConcurrentHashMap<>(); + this.notificationService = sailingNotificationService; final ConstructorParameters constructorParameters = constructorParametersProvider.apply(this); this.domainObjectFactory = constructorParameters.getDomainObjectFactory(); this.masterDataClassLoaders.add(this.getClass().getClassLoader()); @@ -606,7 +616,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes Thread.NORM_PRIORITY, /* daemon */true)); // TODO: initialize smart-future-cache for simulation-results and add to simulation-service simulationService = SimulationServiceFactory.INSTANCE.getService(simulatorExecutor, this); - this.raceLogReplicator = new RaceLogReplicator(this); + this.raceLogReplicator = new RaceLogReplicatorAndNotifier(this); this.regattaLogReplicator = new RegattaLogReplicator(this); this.raceLogScoringReplicator = new RaceLogScoringReplicator(this); this.mediaLibrary = new MediaLibrary(); @@ -777,6 +787,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes leaderboard.addRaceColumnListener(raceLogReplicator); leaderboard.addRaceColumnListener(raceLogScoringReplicator); } + final LeaderboardScoreCorrectionNotifier scoreCorrectionListener = new LeaderboardScoreCorrectionNotifier(leaderboard); + scoreCorrectionListenersByLeaderboard.put(leaderboard, scoreCorrectionListener); + leaderboard.getScoreCorrection().addScoreCorrectionListener(scoreCorrectionListener); } private void loadStoredLeaderboardsAndGroups() { @@ -1008,6 +1021,10 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes if (leaderboard != null) { leaderboard.removeRaceColumnListener(raceLogReplicator); leaderboard.removeRaceColumnListener(raceLogScoringReplicator); + final ScoreCorrectionListener scoreCorrectionListener = scoreCorrectionListenersByLeaderboard.remove(leaderboard); + if (scoreCorrectionListener != null) { + leaderboard.getScoreCorrection().removeScoreCorrectionListener(scoreCorrectionListener); + } mongoObjectFactory.removeLeaderboard(leaderboardName); syncGroupsAfterLeaderboardRemove(leaderboardName, true); if (leaderboard instanceof FlexibleLeaderboard) { @@ -1537,7 +1554,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * service, the service adds the tracked race to the default leaderboard and links it to the leaderboard columns * that were previously connected to it. Additionally, a {@link RaceChangeListener} is added to the * {@link TrackedRace} which is responsible for triggering the replication of all relevant changes to the tracked - * race. When a tracked race is removed, the {@link TrackedRaceReplicator} that was added as listener to that + * race. When a tracked race is removed, the {@link TrackedRaceReplicatorAndNotifier} that was added as listener to that * tracked race is removed again. * * A {@link PolarFixCacheUpdater} is added to every race so that polar fixes are aggregated when new GPS fixes @@ -1549,18 +1566,18 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private class RaceAdditionListener implements RaceListener, Serializable { private static final long serialVersionUID = 1036955460477000265L; - private final Map trackedRaceReplicators; + private final Map trackedRaceReplicators; private final Map polarFixCacheUpdaters; public RaceAdditionListener() { - this.trackedRaceReplicators = new HashMap(); + this.trackedRaceReplicators = new HashMap(); this.polarFixCacheUpdaters = new HashMap(); } @Override public void raceRemoved(TrackedRace trackedRace) { - TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace); + TrackedRaceReplicatorAndNotifier trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace); if (trackedRaceReplicator != null) { trackedRace.removeListener(trackedRaceReplicator); } @@ -1578,7 +1595,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes trackedRace.getMillisecondsOverWhichToAverageSpeed()); replicate(op); linkRaceToConfiguredLeaderboardColumns(trackedRace); - TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace); + TrackedRaceReplicatorAndNotifier trackedRaceReplicator = new TrackedRaceReplicatorAndNotifier(trackedRace); trackedRaceReplicators.put(trackedRace, trackedRaceReplicator); trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, true); @@ -1591,6 +1608,97 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } } } + + /** + * A score correction listener for a leaderboard that notifies interested users through the + * {@link RacingEventServiceImpl#notificationService} if one is available. + * + * @author Axel Uhl (d043530) + * + */ + private class LeaderboardScoreCorrectionNotifier implements ScoreCorrectionListener { + /** + * We don't want to flood the users with notifications about what's basically caused by the + * same original event. For example, when many single score correction updates are applied to + * the same leaderboard, we don't want to notify users for each such change if they are generally + * interested in new results for that leaderboard or this boat class. + */ + private final Duration HOW_LONG_BETWEEN_TWO_NOTIFICATIONS_FOR_SIMILAR_EVENT = Duration.ONE_MINUTE.times(5); + + private TimePoint lastNotificationForLeaderboard; + + private final ConcurrentHashMap lastNotificationForCompetitor; + + private final Leaderboard leaderboard; + + /** + * Callers are expected to {@link SettableScoreCorrection#addScoreCorrectionListener(ScoreCorrectionListener) + * register} this listener as a {@link ScoreCorrectionListener} themselves. + */ + public LeaderboardScoreCorrectionNotifier(Leaderboard leaderboard) { + this.leaderboard = leaderboard; + this.lastNotificationForCompetitor = new ConcurrentHashMap<>(); + } + + @Override + public void correctedScoreChanced(Competitor competitor, RaceColumn raceColumn, Double oldCorrectedScore, + Double newCorrectedScore) { + notifyForCompetitorIfNotAlreadyNotifiedRecently(competitor, raceColumn); + } + + @Override + public void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { + notifyForCompetitorIfNotAlreadyNotifiedRecently(competitor, raceColumn); + } + + @Override + public void carriedPointsChanged(Competitor competitor, Double oldCarriedPoints, Double newCarriedPoints) { + notifyForCompetitorIfNotAlreadyNotifiedRecently(competitor, /* no raceColumn in case of carried points */ null); + } + + @Override + public void isSuppressedChanged(Competitor competitor, boolean newIsSuppressed) { + // do nothing + } + + @Override + public void timePointOfLastCorrectionsValidityChanged(TimePoint oldTimePointOfLastCorrectionsValidity, + TimePoint newTimePointOfLastCorrectionsValidity) { + notifyForLeaderboardIfNotAlreadyNotifiedRecently(); + } + + @Override + public void commentChanged(String oldComment, String newComment) { + notifyForLeaderboardIfNotAlreadyNotifiedRecently(); + } + + private void notifyForLeaderboardIfNotAlreadyNotifiedRecently() { + final TimePoint now = MillisecondsTimePoint.now(); + if (notificationService != null && (lastNotificationForLeaderboard == null || + lastNotificationForLeaderboard.until(now).compareTo(HOW_LONG_BETWEEN_TWO_NOTIFICATIONS_FOR_SIMILAR_EVENT) >= 0)) { + scheduler.execute(()->notificationService.notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable( + leaderboard.getBoatClass(), leaderboard)); + lastNotificationForLeaderboard = now; + } + } + + /** + * @param raceColumn + * may be {@code null} which means that something may have changed for the competitor outside of a + * specific race column, such as the carried points + */ + private void notifyForCompetitorIfNotAlreadyNotifiedRecently(Competitor competitor, RaceColumn raceColumn) { + final TimePoint now = MillisecondsTimePoint.now(); + if (notificationService != null && (!lastNotificationForCompetitor.containsKey(competitor) || + lastNotificationForCompetitor.get(competitor).until(now).compareTo(HOW_LONG_BETWEEN_TWO_NOTIFICATIONS_FOR_SIMILAR_EVENT) >= 0)) { + scheduler.execute(()->notificationService.notifyUserOnCompetitorScoreCorrections(competitor, leaderboard)); + lastNotificationForCompetitor.put(competitor, now); + } + // a change to a single competitor also means a change to the leaderboard + notifyForLeaderboardIfNotAlreadyNotifiedRecently(); + } + + } private class PolarFixCacheUpdater extends AbstractRaceChangeListener { @@ -1619,10 +1727,19 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } - private class TrackedRaceReplicator implements RaceChangeListener { + /** + * When changes occur on a {@link TrackedRace}, this object will be notified in its role of being a + * {@link RaceChangeListener}. It does two things: replicate the changes to replica servers and, potentially, if + * this is a replica, back to a master; notify users who expressed a corresponding interest about the change if we + * have a {@link RacingEventServiceImpl##notificationService} available. + * + * @author Axel Uhl (d043530) + * + */ + private class TrackedRaceReplicatorAndNotifier implements RaceChangeListener { private final TrackedRace trackedRace; - public TrackedRaceReplicator(TrackedRace trackedRace) { + public TrackedRaceReplicatorAndNotifier(TrackedRace trackedRace) { this.trackedRace = trackedRace; } @@ -1648,14 +1765,26 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public void startOfRaceChanged(TimePoint oldStartOfRace, TimePoint newStartOfRace) { - // no action required; the update signaled by this call is implicit; for explicit updates + // no replication action required; the update signaled by this call is implicit; for explicit updates // see raceTimesChanged(TimePoint, TimePoint, TimePoint). + + if (newStartOfRace != null && newStartOfRace.after(MillisecondsTimePoint.now())) { + scheduler.execute(()-> + // Notify interested users if the new start time is in the future + notificationService.notifyUserOnBoatClassUpcomingRace(trackedRace.getRace().getBoatClass(), + getMostAppropriateLeaderboard(), getMostAppropriateRaceColumn(), getMostAppropriateFleet(), newStartOfRace)); + } } @Override public void finishedTimeChanged(TimePoint oldFinishedTime, TimePoint newFinishedTime) { // no action required; the update signaled by this call is implicit; the race log // updates that led to this change are replicated separately + + scheduler.execute(()-> + // Notify interested users: + notificationService.notifyUserOnBoatClassRaceChangesStateToFinished(trackedRace.getRace().getBoatClass(), trackedRace, + getMostAppropriateLeaderboard(), getMostAppropriateRaceColumn(), getMostAppropriateFleet())); } @Override @@ -1713,6 +1842,14 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes public void markPassingReceived(Competitor competitor, Map oldMarkPassings, Iterable markPassings) { replicate(new UpdateMarkPassings(getRaceIdentifier(), competitor, markPassings)); + final MarkPassing last = Util.last(markPassings); + if (last != null && last.getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint() && + trackedRace.getStatus().getStatus() != TrackedRaceStatusEnum.LOADING) { + scheduler.execute(() -> + // Notify interested users: + notificationService.notifyUserOnCompetitorPassesFinish(competitor, trackedRace, + getMostAppropriateLeaderboard(), getMostAppropriateRaceColumn(), getMostAppropriateFleet())); + } } @Override @@ -1748,6 +1885,62 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes public void raceLogDetached(RaceLog raceLog) { // no action required } + + private Leaderboard getMostAppropriateLeaderboard() { + final Triple slot = findMostAppropriateLeaderboardSlot(); + return slot == null ? null : slot.getA(); + } + + private RaceColumn getMostAppropriateRaceColumn() { + final Triple slot = findMostAppropriateLeaderboardSlot(); + return slot == null ? null : slot.getB(); + } + + private Fleet getMostAppropriateFleet() { + final Triple slot = findMostAppropriateLeaderboardSlot(); + return slot == null ? null : slot.getC(); + } + + /** + * When all we have is a {@link #trackedRace} and we're looking for a slot in a leaderboard, we have to + * search the enclosing {@link RacingEventService} for a leaderboard that has the {@link #trackedRace} + * in a {@link RaceColumn}/{@link Fleet} slot.

    + * + * As a first approximation we'll use the fact that a {@link RegattaLeaderboard}'s name is derived + * from the {@link Regatta} and as such can be looked up in constant time. Only if such a regatta + * leaderboard is not found, a search across all leaderboards will need to be carried out.

    + * + * Note that this method should be called in a background thread that runs outside of the call stack + * of the notification sent to this {@link RaceChangeListener}, ideally as a task in an executor. + * This will avoid performance hits due to an attempt to send out notifications. + */ + private Triple findMostAppropriateLeaderboardSlot() { + final Regatta regatta = trackedRace.getTrackedRegatta().getRegatta(); + final String regattaLeaderboardName = RegattaLeaderboardImpl.getLeaderboardNameForRegatta(regatta); + final Leaderboard regattaLeaderboard = getLeaderboardByName(regattaLeaderboardName); + Leaderboard leaderboard = null; + Pair raceColumnAndFleet = null; + if (regattaLeaderboard != null) { + leaderboard = regattaLeaderboard; + raceColumnAndFleet = regattaLeaderboard.getRaceColumnAndFleet(trackedRace); + } else { + for (final Leaderboard l : getLeaderboards().values()) { + final Pair rcaf = l.getRaceColumnAndFleet(trackedRace); + if (rcaf != null) { + leaderboard = l; + raceColumnAndFleet = rcaf; + break; + } + } + } + final Triple result; + if (leaderboard != null && raceColumnAndFleet != null) { + result = new Triple<>(leaderboard, raceColumnAndFleet.getA(), raceColumnAndFleet.getB()); + } else { + result = null; + } + return result; + } } /** @@ -2658,6 +2851,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes LockUtil.lockForWrite(leaderboardsByNameLock); try { leaderboardsByName.clear(); + scoreCorrectionListenersByLeaderboard.clear(); } finally { LockUtil.unlockAfterWrite(leaderboardsByNameLock); } @@ -2665,6 +2859,10 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes mediaLibrary.clear(); competitorStore.clear(); remoteSailingServerSet.clear(); + if (notificationService != null) { + notificationService.stop(); + notificationService = new EmptySailingNotificationService(); + } } // Used for TESTING only @@ -2672,12 +2870,12 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes public Event addEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venue, boolean isPublic, UUID id) { Event result = createEventWithoutReplication(eventName, eventDescription, startDate, endDate, venue, isPublic, - id, /* officialWebsiteURL */null, /* sailorsInfoWebsiteURLAsString */null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList()); + id, /* officialWebsiteURL */null, /* baseURL */null, + /* sailorsInfoWebsiteURLAsString */null, /* images */Collections. emptyList(), /* videos */Collections. emptyList()); replicate(new CreateEvent(eventName, eventDescription, startDate, endDate, venue, isPublic, id, - /* officialWebsiteURLAsString */null, /* sailorsInfoWebsiteURLAsString */null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList(), - /* leaderboardGroupIds */ Collections. emptyList())); + /* officialWebsiteURLAsString */null, /*baseURL*/null, + /* sailorsInfoWebsiteURLAsString */null, /* images */Collections. emptyList(), + /* videos */Collections. emptyList(), /* leaderboardGroupIds */ Collections. emptyList())); return result; } @@ -2688,12 +2886,13 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public Event createEventWithoutReplication(String eventName, String eventDescription, TimePoint startDate, - TimePoint endDate, String venue, boolean isPublic, UUID id, URL officialWebsiteURL, Map sailorsInfoWebsiteURLs, - Iterable images, Iterable videos) { + TimePoint endDate, String venue, boolean isPublic, UUID id, URL officialWebsiteURL, URL baseURL, + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos) { Event result = new EventImpl(eventName, startDate, endDate, venue, isPublic, id); addEvent(result); result.setDescription(eventDescription); result.setOfficialWebsiteURL(officialWebsiteURL); + result.setBaseURL(baseURL); result.setSailorsInfoWebsiteURLs(sailorsInfoWebsiteURLs); result.setImages(images); result.setVideos(videos); @@ -2711,8 +2910,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, - String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, Map sailorsInfoWebsiteURLs, - Iterable images, Iterable videos) { + String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL baseURL, + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos) { final Event event = eventsById.get(id); if (event == null) { throw new IllegalArgumentException("Sailing event with ID " + id + " does not exist."); @@ -2735,6 +2934,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } event.setLeaderboardGroups(leaderboardGroups); event.setOfficialWebsiteURL(officialWebsiteURL); + event.setBaseURL(baseURL); event.setSailorsInfoWebsiteURLs(sailorsInfoWebsiteURLs); event.setImages(images); event.setVideos(videos); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java index 928825e5055..f29f2329e91 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java @@ -20,7 +20,7 @@ import com.sap.sailing.server.operationaltransformation.RecordRegattaLogEventOnR * passed to this object's constructor and send a {@link RecordRegattaLogEventOnRegatta} or a * {@link RecordRegattaLogEventOnFlexibleLeaderboard} operation to all replicas. * - * This class is a modified version of {@link RaceLogReplicator} + * This class is a modified version of {@link RaceLogReplicatorAndNotifier} * */ public class RegattaLogReplicator implements RegattaLikeListener { diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/AbstractBoatClassNotificationSet.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/AbstractBoatClassNotificationSet.java new file mode 100644 index 00000000000..0ac76467f4b --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/AbstractBoatClassNotificationSet.java @@ -0,0 +1,47 @@ +package com.sap.sailing.server.impl.preferences; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.osgi.framework.BundleContext; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreference; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreferences; +import com.sap.sse.security.PreferenceObjectBasedNotificationSet; +import com.sap.sse.security.UserStore; + +/** + * {@link PreferenceObjectBasedNotificationSet} for associations of {@link BoatClass} to a set of users to notify about + * specific events for the {@link BoatClass}. Subclasses define the concrete case of notification based on the flags in + * {@link BoatClassNotificationPreference} via implementing {@link #shouldNotifyFor(BoatClassNotificationPreference)}. + */ +public abstract class AbstractBoatClassNotificationSet + extends PreferenceObjectBasedNotificationSet { + + public AbstractBoatClassNotificationSet(UserStore store) { + super(BoatClassNotificationPreferences.PREF_NAME, store); + } + + /** + * Constructor used to automatically track {@link UserStore} as OSGi service. + */ + public AbstractBoatClassNotificationSet(BundleContext bundleContext) { + super(BoatClassNotificationPreferences.PREF_NAME, bundleContext); + } + + @Override + protected Collection calculateObjectsToNotify(BoatClassNotificationPreferences preference) { + Set result = new HashSet<>(); + for (BoatClassNotificationPreference pref : preference.getBoatClasses()) { + if (shouldNotifyFor(pref)) { + result.add(pref.getBoatClass()); + } + } + return result; + } + + protected abstract boolean shouldNotifyFor(BoatClassNotificationPreference pref); + +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/AbstractCompetitorNotificationSet.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/AbstractCompetitorNotificationSet.java new file mode 100644 index 00000000000..2b35968c0b6 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/AbstractCompetitorNotificationSet.java @@ -0,0 +1,47 @@ +package com.sap.sailing.server.impl.preferences; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.osgi.framework.BundleContext; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreference; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences; +import com.sap.sse.security.PreferenceObjectBasedNotificationSet; +import com.sap.sse.security.UserStore; + +/** + * {@link PreferenceObjectBasedNotificationSet} for associations of {@link Competitor} to a set of users to notify about + * specific events for the {@link Competitor}. Subclasses define the concrete case of notification based on the flags in + * {@link CompetitorNotificationPreference} via implementing {@link #shouldNotifyFor(CompetitorNotificationPreference)}. + */ +public abstract class AbstractCompetitorNotificationSet + extends PreferenceObjectBasedNotificationSet { + + public AbstractCompetitorNotificationSet(UserStore store) { + super(CompetitorNotificationPreferences.PREF_NAME, store); + } + + /** + * Constructor used to automatically track {@link UserStore} as OSGi service. + */ + public AbstractCompetitorNotificationSet(BundleContext bundleContext) { + super(CompetitorNotificationPreferences.PREF_NAME, bundleContext); + } + + @Override + protected Collection calculateObjectsToNotify(CompetitorNotificationPreferences preference) { + Set result = new HashSet<>(); + for (CompetitorNotificationPreference pref : preference.getCompetitors()) { + if (shouldNotifyFor(pref)) { + result.add(pref.getCompetitor()); + } + } + return result; + } + + protected abstract boolean shouldNotifyFor(CompetitorNotificationPreference pref); + +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/BoatClassResultsNotificationSet.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/BoatClassResultsNotificationSet.java new file mode 100644 index 00000000000..25870d5919e --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/BoatClassResultsNotificationSet.java @@ -0,0 +1,30 @@ +package com.sap.sailing.server.impl.preferences; + +import org.osgi.framework.BundleContext; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreference; +import com.sap.sse.security.UserStore; + +/** + * Notification set that defines the association of a {@link BoatClass} to the set of Users that need to be notified + * whenever new results are available for a {@link BoatClass}. + */ +public class BoatClassResultsNotificationSet extends AbstractBoatClassNotificationSet { + + public BoatClassResultsNotificationSet(UserStore store) { + super(store); + } + + /** + * Constructor used to automatically track {@link UserStore} as OSGi service. + */ + public BoatClassResultsNotificationSet(BundleContext bundleContext) { + super(bundleContext); + } + + @Override + protected boolean shouldNotifyFor(BoatClassNotificationPreference pref) { + return pref.isNotifyAboutResults(); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/BoatClassUpcomingRaceNotificationSet.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/BoatClassUpcomingRaceNotificationSet.java new file mode 100644 index 00000000000..05745a32813 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/BoatClassUpcomingRaceNotificationSet.java @@ -0,0 +1,30 @@ +package com.sap.sailing.server.impl.preferences; + +import org.osgi.framework.BundleContext; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPreference; +import com.sap.sse.security.UserStore; + +/** + * Notification set that defines the association of a {@link BoatClass} to the set of Users that need to be notified + * whenever a race is known to start in the near future for a {@link BoatClass}. + */ +public class BoatClassUpcomingRaceNotificationSet extends AbstractBoatClassNotificationSet { + + public BoatClassUpcomingRaceNotificationSet(UserStore store) { + super(store); + } + + /** + * Constructor used to automatically track {@link UserStore} as OSGi service. + */ + public BoatClassUpcomingRaceNotificationSet(BundleContext bundleContext) { + super(bundleContext); + } + + @Override + protected boolean shouldNotifyFor(BoatClassNotificationPreference pref) { + return pref.isNotifyAboutUpcomingRaces(); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/CompetitorResultsNotificationSet.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/CompetitorResultsNotificationSet.java new file mode 100644 index 00000000000..db100bfcc14 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/CompetitorResultsNotificationSet.java @@ -0,0 +1,31 @@ +package com.sap.sailing.server.impl.preferences; + +import org.osgi.framework.BundleContext; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreference; +import com.sap.sse.security.UserStore; + +/** + * Notification set that defines the association of a {@link Competitor} to the set of Users that need to be notified + * whenever new results are available for a {@link Competitor}. + */ +public class CompetitorResultsNotificationSet extends AbstractCompetitorNotificationSet { + + public CompetitorResultsNotificationSet(UserStore store) { + super(store); + } + + /** + * Constructor used to automatically track {@link UserStore} as OSGi service. + */ + public CompetitorResultsNotificationSet(BundleContext bundleContext) { + super(bundleContext); + } + + @Override + protected boolean shouldNotifyFor(CompetitorNotificationPreference pref) { + return pref.isNotifyAboutResults(); + } + +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassNotificationPreference.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassNotificationPreference.java new file mode 100644 index 00000000000..282244bbca4 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassNotificationPreference.java @@ -0,0 +1,49 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.BooleanSetting; + +public class BoatClassNotificationPreference extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -6510535362114348707L; + + private transient BoatClassSetting boatClass; + private transient BooleanSetting notifyAboutUpcomingRaces; + private transient BooleanSetting notifyAboutResults; + + public BoatClassNotificationPreference(DomainFactory domainFactory) { + boatClass = new BoatClassSetting("boatClass", this, domainFactory); + notifyAboutUpcomingRaces = new BooleanSetting("notifyAboutUpcomingRaces", this, false); + notifyAboutResults = new BooleanSetting("notifyAboutResults", this, false); + } + + public BoatClassNotificationPreference(DomainFactory domainFactory, BoatClass boatClass, boolean notifyAboutRaces, + boolean notifyAboutResults) { + this(domainFactory); + this.boatClass.setValue(boatClass); + this.notifyAboutUpcomingRaces.setValue(notifyAboutRaces); + this.notifyAboutResults.setValue(notifyAboutResults); + } + + @Override + protected void addChildSettings() { + // We do not create the Setting instances here, because access to the RacingEventService would not be given. + // Doing this, Java/GWT Serialization isn't working anymore. Because the preferences are only serialized as JSON + // in the backend an transferred as DTO to the frontend, this isn't a problem. Due to usage of BoatClass and + // Competitor domain objects, it wouldn't be GWT compatible anyway. + // The usage of Java Serialization isn't planned by now, either. + } + + public BoatClass getBoatClass() { + return boatClass.getValue(); + } + + public boolean isNotifyAboutUpcomingRaces() { + return Boolean.TRUE.equals(notifyAboutUpcomingRaces.getValue()); + } + + public boolean isNotifyAboutResults() { + return Boolean.TRUE.equals(notifyAboutResults.getValue()); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassNotificationPreferences.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassNotificationPreferences.java new file mode 100644 index 00000000000..4114b9e7643 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassNotificationPreferences.java @@ -0,0 +1,35 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sailing.server.RacingEventService; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsList; + +public class BoatClassNotificationPreferences extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -3682996540081614053L; + + public static final String PREF_NAME = SailingPreferences.NOTIFICATION_PREFERENCES_PREFIX + "boatclasses"; + + private transient SettingsList boatClasses; + + public BoatClassNotificationPreferences(RacingEventService racingEventService) { + boatClasses = new SettingsList<>("boatClasses", this, + () -> new BoatClassNotificationPreference(racingEventService.getBaseDomainFactory())); + } + + @Override + protected void addChildSettings() { + // We do not create the Setting instances here, because access to the RacingEventService would not be given. + // Doing this, Java/GWT Serialization isn't working anymore. Because the preferences are only serialized as JSON + // in the backend an transferred as DTO to the frontend, this isn't a problem. Due to usage of BoatClass and + // Competitor domain objects, it wouldn't be GWT compatible anyway. + // The usage of Java Serialization isn't planned by now, either. + } + + public Iterable getBoatClasses() { + return boatClasses.getValues(); + } + + public void setBoatClasses(Iterable boatClasses) { + this.boatClasses.setValues(boatClasses); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassSetting.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassSetting.java new file mode 100644 index 00000000000..b34a8d6f163 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/BoatClassSetting.java @@ -0,0 +1,59 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.value.StringValue; +import com.sap.sse.common.settings.value.Value; + +/** + * Specific setting to hold a {@link BoatClass} instance. The competitor is being serialized using the boat classes' + * name and deserialized using the given {@link DomainFactory}. + */ +public class BoatClassSetting extends AbstractValueSetting { + + protected BoatClassSetting(String name, AbstractGenericSerializableSettings settings, DomainFactory domainFactory) { + super(name, settings, null, new Converter(domainFactory)); + } + + public static class Converter implements ValueConverter { + + private final DomainFactory domainFactory; + + public Converter(DomainFactory domainFactory) { + this.domainFactory = domainFactory; + } + + @Override + public Object toJSONValue(BoatClass value) { + return toStringValue(value); + } + + @Override + public BoatClass fromJSONValue(Object jsonValue) { + return fromStringValue((String) jsonValue); + } + + @Override + public String toStringValue(BoatClass value) { + return value == null ? null : value.getDisplayName(); + } + + @Override + public BoatClass fromStringValue(String stringValue) { + return stringValue == null ? null : domainFactory.getOrCreateBoatClass(stringValue); + } + + @Override + public BoatClass fromValue(Value value) { + return fromStringValue(((StringValue) value).getValue()); + } + + @Override + public Value toValue(BoatClass value) { + return new StringValue(toStringValue(value)); + } + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorNotificationPreference.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorNotificationPreference.java new file mode 100644 index 00000000000..8710f452e99 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorNotificationPreference.java @@ -0,0 +1,42 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorStore; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.BooleanSetting; + +public class CompetitorNotificationPreference extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -6510535362114348707L; + + private transient CompetitorSetting competitor; + private transient BooleanSetting notifyAboutResults; + + public CompetitorNotificationPreference(CompetitorStore competitorStore) { + competitor = new CompetitorSetting("competitor", this, competitorStore); + notifyAboutResults = new BooleanSetting("notifyAboutResults", this, false); + } + + public CompetitorNotificationPreference(CompetitorStore competitorStore, Competitor competitor, + boolean notifyAboutResults) { + this(competitorStore); + this.competitor.setValue(competitor); + this.notifyAboutResults.setValue(notifyAboutResults); + } + + @Override + protected void addChildSettings() { + // We do not create the Setting instances here, because access to the RacingEventService would not be given. + // Doing this, Java/GWT Serialization isn't working anymore. Because the preferences are only serialized as JSON + // in the backend an transferred as DTO to the frontend, this isn't a problem. Due to usage of BoatClass and + // Competitor domain objects, it wouldn't be GWT compatible anyway. + // The usage of Java Serialization isn't planned by now, either. + } + + public Competitor getCompetitor() { + return competitor.getValue(); + } + + public boolean isNotifyAboutResults() { + return Boolean.TRUE.equals(notifyAboutResults.getValue()); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorNotificationPreferences.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorNotificationPreferences.java new file mode 100644 index 00000000000..5fa160f0490 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorNotificationPreferences.java @@ -0,0 +1,35 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sailing.server.RacingEventService; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsList; + +public class CompetitorNotificationPreferences extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -3682996540081614053L; + + public static final String PREF_NAME = SailingPreferences.NOTIFICATION_PREFERENCES_PREFIX + "competitors"; + + private transient SettingsList competitors; + + public CompetitorNotificationPreferences(RacingEventService racingEventService) { + competitors = new SettingsList<>("competitors", this, + () -> new CompetitorNotificationPreference(racingEventService.getCompetitorStore())); + } + + @Override + protected void addChildSettings() { + // We do not create the Setting instances here, because access to the RacingEventService would not be given. + // Doing this, Java/GWT Serialization isn't working anymore. Because the preferences are only serialized as JSON + // in the backend an transferred as DTO to the frontend, this isn't a problem. Due to usage of BoatClass and + // Competitor domain objects, it wouldn't be GWT compatible anyway. + // The usage of Java Serialization isn't planned by now, either. + } + + public Iterable getCompetitors() { + return competitors.getValues(); + } + + public void setCompetitors(Iterable competitors) { + this.competitors.setValues(competitors); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorSetting.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorSetting.java new file mode 100644 index 00000000000..0133116b9ef --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/CompetitorSetting.java @@ -0,0 +1,60 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorStore; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.value.StringValue; +import com.sap.sse.common.settings.value.Value; + +/** + * Specific setting to hold a {@link Competitor} instance. The competitor is being serialized as ID and deserialized + * doing as lookup in the given {@link CompetitorStore}. + */ +public class CompetitorSetting extends AbstractValueSetting { + + protected CompetitorSetting(String name, AbstractGenericSerializableSettings settings, + CompetitorStore competitorStore) { + super(name, settings, null, new Converter(competitorStore)); + } + + public static class Converter implements ValueConverter { + + private final CompetitorStore competitorStore; + + public Converter(CompetitorStore competitorStore) { + this.competitorStore = competitorStore; + } + + @Override + public Object toJSONValue(Competitor value) { + return toStringValue(value); + } + + @Override + public Competitor fromJSONValue(Object jsonValue) { + return fromStringValue((String) jsonValue); + } + + @Override + public String toStringValue(Competitor value) { + return value == null ? null : value.getId().toString(); + } + + @Override + public Competitor fromStringValue(String stringValue) { + return stringValue == null ? null : competitorStore.getExistingCompetitorByIdAsString(stringValue); + } + + @Override + public Competitor fromValue(Value value) { + return fromStringValue(((StringValue) value).getValue()); + } + + @Override + public Value toValue(Competitor value) { + return new StringValue(toStringValue(value)); + } + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java new file mode 100644 index 00000000000..54d0392529d --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java @@ -0,0 +1,9 @@ +package com.sap.sailing.server.impl.preferences.model; + +/** + * Defines common prefixes for classes of preferences in SAP Sailing to ensure some level of consistency when defining + * preference keys. + */ +public interface SailingPreferences { + public static final String NOTIFICATION_PREFERENCES_PREFIX = "sailing.notifications."; +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/EmptySailingNotificationService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/EmptySailingNotificationService.java new file mode 100644 index 00000000000..d34b6bd283d --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/EmptySailingNotificationService.java @@ -0,0 +1,39 @@ +package com.sap.sailing.server.notification; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; + +public class EmptySailingNotificationService implements SailingNotificationService { + + @Override + public void stop() { + } + + @Override + public void notifyUserOnBoatClassRaceChangesStateToFinished(BoatClass boatClass, TrackedRace trackedRace, + Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) { + } + + @Override + public void notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(BoatClass boatClass, Leaderboard leaderboard) { + } + + @Override + public void notifyUserOnBoatClassUpcomingRace(BoatClass boatClass, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet, TimePoint when) { + } + + @Override + public void notifyUserOnCompetitorPassesFinish(Competitor competitor, TrackedRace trackedRace, + Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) { + } + + @Override + public void notifyUserOnCompetitorScoreCorrections(Competitor competitor, Leaderboard leaderboard) { + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java new file mode 100644 index 00000000000..f3410aed607 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java @@ -0,0 +1,42 @@ +package com.sap.sailing.server.notification; + +import com.sap.sailing.domain.abstractlog.race.state.RaceState; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.Stoppable; +import com.sap.sse.common.TimePoint; + +public interface SailingNotificationService extends Stoppable { + + /** + * Called when {@link RaceState} changes state to finished. + */ + void notifyUserOnBoatClassRaceChangesStateToFinished(BoatClass boatClass, TrackedRace trackedRace, Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet); + + /** + * Called on score correction for a race with BoatClass if the last score correction was more than one minute. + */ + void notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(BoatClass boatClass, Leaderboard leaderboard); + + /** + * Called whenever a start time is set for a race with {@link BoatClass} through the race's {@link RaceState}. + */ + void notifyUserOnBoatClassUpcomingRace(BoatClass boatClass, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet, TimePoint when); + + /** + * Triggered when a competitor passes the finishing waypoint of a race. + */ + void notifyUserOnCompetitorPassesFinish(Competitor competitor, TrackedRace trackedRace, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet); + + /** + * Triggered when a score correction is set for competitor on any leaderboard. + */ + void notifyUserOnCompetitorScoreCorrections(Competitor competitor, Leaderboard leaderboard); + +} \ No newline at end of file diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/NotificationMailTemplate.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/NotificationMailTemplate.java new file mode 100644 index 00000000000..27d6b5a67ee --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/NotificationMailTemplate.java @@ -0,0 +1,51 @@ +package com.sap.sailing.server.notification.impl; + +import java.net.URL; +import java.util.Arrays; + +import com.sap.sse.common.Util.Pair; + +public class NotificationMailTemplate { + + private final String subject; + private final String title; + private final String text; + private final String serverBaseUrl; + private final Iterable> labelsAndLinkUrls; + + @SafeVarargs + public NotificationMailTemplate(String subject, String text, URL serverBaseUrl, + Pair... labelsAndLinkUrls) { + this(subject, null, text, serverBaseUrl, labelsAndLinkUrls); + } + + @SafeVarargs + public NotificationMailTemplate(String subject, String title, String text, URL serverBaseUrl, + Pair... labelsAndLinkUrls) { + this.subject = subject; + this.title = title; + this.text = text; + this.serverBaseUrl = serverBaseUrl.toString(); + this.labelsAndLinkUrls = Arrays.asList(labelsAndLinkUrls); + } + + public String getServerBaseUrl() { + return serverBaseUrl; + } + + public String getSubject() { + return subject; + } + + public String getTitle() { + return title; + } + + public String getText() { + return text; + } + + public Iterable> getLabelsAndLinkUrls() { + return labelsAndLinkUrls; + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/NotificationSetNotification.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/NotificationSetNotification.java new file mode 100644 index 00000000000..0938e6e679e --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/NotificationSetNotification.java @@ -0,0 +1,167 @@ +package com.sap.sailing.server.notification.impl; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +import javax.mail.BodyPart; +import javax.mail.Multipart; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; +import javax.mail.util.ByteArrayDataSource; + +import com.sap.sse.common.Util.Pair; +import com.sap.sse.i18n.impl.ResourceBundleStringMessagesImpl; +import com.sap.sse.mail.MailService; +import com.sap.sse.mail.queue.MailNotification; +import com.sap.sse.security.PreferenceObjectBasedNotificationSet; +import com.sap.sse.security.User; + +public abstract class NotificationSetNotification implements MailNotification { + private static final Logger logger = Logger.getLogger(NotificationSetNotification.class.getName()); + + private static final String TEMPLATE_FILE = "notification-mail-template.html"; + private static final String LOGO_FILE = "sap_logo_header.png"; + + private static final String TEMPLATE = loadTemplateFile(); + private static final byte[] LOGO_BYTES = loadLogoFile(); + + private static String loadTemplateFile() { + StringBuilder content = new StringBuilder(); + try (BufferedReader in = new BufferedReader(new InputStreamReader( + NotificationSetNotification.class.getResourceAsStream(TEMPLATE_FILE), StandardCharsets.UTF_8))) { + String line; + while ((line = in.readLine()) != null) { + content.append(line).append("\n"); + } + } catch (IOException exc) { + logger.log(Level.SEVERE, "Error while loading notification mail template!", exc); + } + return content.toString(); + } + + private static byte[] loadLogoFile() { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream in = NotificationSetNotification.class.getResourceAsStream(LOGO_FILE)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + return baos.toByteArray(); + } catch (IOException exc) { + logger.log(Level.SEVERE, "Error while loading notification mail template!", exc); + return null; + } + } + + private final T objectToNotifyAbout; + private final PreferenceObjectBasedNotificationSet associatedNotificationSet; + private static final ResourceBundleStringMessagesImpl messages = new ResourceBundleStringMessagesImpl( + SailingNotificationServiceImpl.STRING_MESSAGES_BASE_NAME, + NotificationSetNotification.class.getClassLoader()); + + public NotificationSetNotification(T objectToNotifyAbout, PreferenceObjectBasedNotificationSet associatedNotificationSet) { + this.objectToNotifyAbout = objectToNotifyAbout; + this.associatedNotificationSet = associatedNotificationSet; + + } + + @Override + public void sendNotifications(final MailService mailService) { + // TODO idea (c) by Axel Uhl: In case of performance/mail queue problems: group mail addresses by locale and + // send batches with the actual mail addresses as bcc? + associatedNotificationSet.forUsersWithVerifiedEmailMappedTo(objectToNotifyAbout, (user) -> { + Locale locale = user.getLocaleOrDefault(); + Multipart multipart = new MimeMultipart(); + BodyPart bodyPart = new MimeBodyPart(); + BodyPart messageImagePart = new MimeBodyPart(); + try { + // TODO: add cid:saplogo + NotificationMailTemplate mailTemplate = getMailTemplate(objectToNotifyAbout, locale); + bodyPart.setContent(getMailContent(mailTemplate, user, locale), "text/html"); + multipart.addBodyPart(bodyPart); + + DataSource imageDs = new ByteArrayDataSource(LOGO_BYTES, "image/png"); + messageImagePart.setDataHandler(new DataHandler(imageDs)); + messageImagePart.setHeader("Content-ID", "saplogo"); + messageImagePart.setHeader("Content-Disposition", "inline;filename=\"saplogo.png\""); + multipart.addBodyPart(messageImagePart); + + mailService.sendMail(user.getEmail(), mailTemplate.getSubject(), multipart); + } catch (Exception e) { + logger.log(Level.SEVERE, + "Could not send mail notification for \"" + objectToNotifyAbout + "\" to user \" + user + \"", e); + } + }); + } + + private String getMailContent(NotificationMailTemplate notificationMailTemplate, User user, Locale locale) { + StringBuilder bodyContent = new StringBuilder(); + if (notificationMailTemplate.getTitle() != null) { + bodyContent.append("

    ") + .append(notificationMailTemplate.getTitle()) + .append("

    "); + } + String name = user.getFullName() == null || user.getFullName().isEmpty() ? user.getName() : user.getFullName(); + bodyContent.append("

    ") + .append(htmlify(messages.get(locale, "salutation", name))) + .append("

    "); + bodyContent.append("

    ") + .append(htmlify(notificationMailTemplate.getText())) + .append("

    "); + for (Pair link : notificationMailTemplate.getLabelsAndLinkUrls()) { + bodyContent.append(""); + } + String siteLink = "" + + notificationMailTemplate.getServerBaseUrl() + ""; + StringBuilder footerLinks = new StringBuilder(); + footerLinks.append("") + .append(htmlify(messages.get(locale, "userProfile"))) + .append("") + .append(" | "); + footerLinks.append("") + .append(htmlify(messages.get(locale, "imprint"))) + .append("") + .append(" | "); + footerLinks.append("") + .append(htmlify(messages.get(locale, "privacy"))) + .append(""); + + String subscriptionInformation = htmlify(messages.get(locale, "subscriptionInformation")); + return TEMPLATE + .replace("${title}", notificationMailTemplate.getSubject()) + .replace("${content}", bodyContent.toString()) + .replace("${subscription_information}", subscriptionInformation) + .replace("${site}", siteLink) // + .replace("${footer_links}", footerLinks.toString()) + ; + } + + private String htmlify(String source) { + StringBuilder result = new StringBuilder(); + for (char c : source.toCharArray()) { + result.append(("\"<>&".indexOf(c) >= 0 || c > 127) ? ( "&#" + (int) c + ";") : Character.toString(c)); + } + return result.toString(); + } + + protected abstract NotificationMailTemplate getMailTemplate(T objectToNotifyAbout, Locale locale); + +} \ No newline at end of file diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java new file mode 100644 index 00000000000..0088e0cf42f --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java @@ -0,0 +1,331 @@ +package com.sap.sailing.server.notification.impl; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.DateFormat; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.logging.Logger; + +import org.osgi.framework.BundleContext; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.impl.preferences.BoatClassResultsNotificationSet; +import com.sap.sailing.server.impl.preferences.BoatClassUpcomingRaceNotificationSet; +import com.sap.sailing.server.impl.preferences.CompetitorResultsNotificationSet; +import com.sap.sailing.server.notification.SailingNotificationService; +import com.sap.sse.common.Stoppable; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.i18n.impl.ResourceBundleStringMessagesImpl; +import com.sap.sse.mail.queue.MailQueue; +import com.sap.sse.security.UserStore; + +public class SailingNotificationServiceImpl implements SailingNotificationService { + public static final String STRING_MESSAGES_BASE_NAME = "stringmessages/StringMessages"; + + private static final Logger logger = Logger.getLogger(SailingNotificationServiceImpl.class.getName()); + + private final Set toStop = new HashSet<>(); + private final MailQueue mailQueue; + private RacingEventService racingEventService; + private final ResourceBundleStringMessagesImpl messages; + + private final BoatClassResultsNotificationSet boatClassResults; + private final BoatClassUpcomingRaceNotificationSet boatClassUpcomingRace; + private final CompetitorResultsNotificationSet competitorResults; + private final URL defaultBaseURL; + + public SailingNotificationServiceImpl(BundleContext bundleContext, MailQueue mailQueue) throws MalformedURLException { + this(mailQueue, + new BoatClassResultsNotificationSet(bundleContext), + new BoatClassUpcomingRaceNotificationSet(bundleContext), + new CompetitorResultsNotificationSet(bundleContext)); + } + + /** + * Constructor used for unit tests to not need BundelContext but directly work with a given UserStore. + * @throws MalformedURLException + */ + public SailingNotificationServiceImpl(UserStore userStore, MailQueue mailQueue) throws MalformedURLException { + this(mailQueue, + new BoatClassResultsNotificationSet(userStore), + new BoatClassUpcomingRaceNotificationSet(userStore), + new CompetitorResultsNotificationSet(userStore)); + } + + public SailingNotificationServiceImpl(MailQueue mailQueue, BoatClassResultsNotificationSet boatClassResults, + BoatClassUpcomingRaceNotificationSet boatClassUpcomingRace, + CompetitorResultsNotificationSet competitorResults) throws MalformedURLException { + this.mailQueue = mailQueue; + this.messages = new ResourceBundleStringMessagesImpl(STRING_MESSAGES_BASE_NAME, + this.getClass().getClassLoader()); + this.defaultBaseURL = new URL("https://www.sapsailing.com"); + + toStop.add(this.boatClassResults = boatClassResults); + toStop.add(this.boatClassUpcomingRace = boatClassUpcomingRace); + toStop.add(this.competitorResults = competitorResults); + } + + @Override + public void stop() { + toStop.forEach(Stoppable::stop); + } + + public void setRacingEventService(RacingEventServiceImpl racingEventService) { + this.racingEventService = racingEventService; + } + + /** + * Calculates the best matching {@link Event} and {@link LeaderboardGroup} for the given {@link Leaderboard}. If + * there is just one Event/LeaderboardGroup, this pair is returned. If an event series is found and the associated + * Event can be obtained, this combination is returned. The first Event of the series is used otherwise. + */ + private Pair calculateAssociatedEventForLeaderboard(final Leaderboard leaderboard) { + Set foundEvents = new LinkedHashSet<>(); + Set foundLeaderboardGroups = new LinkedHashSet<>(); + racingEventService.getAllEvents().forEach(event -> { + event.getLeaderboardGroups().forEach(leaderboardGroup -> { + if (Util.contains(leaderboardGroup.getLeaderboards(), leaderboard)) { + foundEvents.add(event); + foundLeaderboardGroups.add(leaderboardGroup); + } + }); + }); + int foundEventsCount = foundEvents.size(); + if (foundEventsCount == 1) { + // if multiple LeaderboardGroups of the single event reference the same leaderboard, we just use the + // first LeaderboardGroup. This could be a non optimal match but helps to e.g. construct valid links. + return new Pair<>(Util.get(foundEvents, 0), Util.get(foundLeaderboardGroups, 0)); + } else if (foundEventsCount > 1) { + // could be a series + for (final LeaderboardGroup leaderboardGroup : foundLeaderboardGroups) { + if (leaderboardGroup.hasOverallLeaderboard()) { + CourseArea defaultCourseArea = leaderboard.getDefaultCourseArea(); + if (defaultCourseArea != null) { + for (Event event : foundEvents) { + if (Util.contains(event.getVenue().getCourseAreas(), defaultCourseArea)) { + return new Pair<>(event, leaderboardGroup); + } + } + } + } + // Event <-> Leaderboard association is not set up correctly. The UI will show the Leaderboard for + // multiple Events. So any of the found Events is ok for this case. + return new Pair<>(Util.get(foundEvents, 0), Util.get(foundLeaderboardGroups, 0)); + } + } + // No associated event found or association is ambiguous + return null; + } + + /** + * Calculated the best matching Event/LeaderBoardGroup pair via + * {@link #calculateAssociatedEventForLeaderboard(Leaderboard)} and calls the given consumer with these instances. + * If the racingEventService isn't already set, this will do nothing. + */ + private void doWithEvent(Leaderboard leaderboard, BiConsumer consumer) { + if (racingEventService == null) { + logger.severe( + "Can't send notifications if " + getClass().getSimpleName() + ".racingEventService isn't set"); + return; + } + Pair eventAndLeaderboardGroup = calculateAssociatedEventForLeaderboard(leaderboard); + if (eventAndLeaderboardGroup != null) { + consumer.accept(eventAndLeaderboardGroup.getA(), eventAndLeaderboardGroup.getB()); + } + } + + private String calculateRaceDescription(Locale locale, Event event, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet) { + String raceName = raceColumn.getName(); + String eventName = event.getName(); + String leaderboardDisplayName = leaderboard.getDisplayName() != null ? leaderboard.getDisplayName() + : leaderboard.getName(); + + if (Util.size(raceColumn.getFleets()) > 1) { + return messages.get(locale, "raceInFleetInRegattaOfEvent", raceName, leaderboardDisplayName, eventName, + fleet.getName()); + } else { + return messages.get(locale, "raceInRegattaOfEvent", raceName, leaderboardDisplayName, eventName); + + } + } + + private String calculateLeaderboardDescription(Locale locale, Event event, Leaderboard leaderboard) { + String eventName = event.getName(); + String leaderboardDisplayName = leaderboard.getDisplayName() != null ? leaderboard.getDisplayName() + : leaderboard.getName(); + + return messages.get(locale, "leaderboardOfEvent", leaderboardDisplayName, eventName); + } + + private Pair createRaceBoardShowRaceLink(TrackedRace trackedRace, Leaderboard leaderboard, + Event event, LeaderboardGroup leaderboardGroup, Locale locale) { + return createRaceBoardLink(trackedRace, leaderboard, event, leaderboardGroup, "PLAYER", + "raceboardShowRaceLinkTitle", locale); + } + + private Pair createRaceBoardRaceAnalysisLink(TrackedRace trackedRace, Leaderboard leaderboard, + Event event, LeaderboardGroup leaderboardGroup, Locale locale) { + return createRaceBoardLink(trackedRace, leaderboard, event, leaderboardGroup, "FULL_ANALYSIS", + "raceboardRaceAnalysisLinkTitle", locale); + } + + private Pair createRaceBoardLink(TrackedRace trackedRace, Leaderboard leaderboard, Event event, + LeaderboardGroup leaderboardGroup, String raceboardMode, String labelMessageKey, Locale locale) { + RegattaAndRaceIdentifier raceIdentifier = trackedRace.getRaceIdentifier(); + String link = getBaseURL(event).toString() + "/gwt/RaceBoard.html?locale=" + locale.toLanguageTag() + + "&eventId=" + event.getId() + "&leaderboardName=" + leaderboard.getName() + + "&leaderboardGroupName=" + leaderboardGroup.getName() + "&raceName=" + + raceIdentifier.getRaceName() + "&showMapControls=true&viewShowNavigationPanel=true®attaName=" + + raceIdentifier.getRegattaName() + "&mode=" + raceboardMode; + return new Pair(messages.get(locale, labelMessageKey), link); + } + + private Pair createHomeRacesListLink(Leaderboard leaderboard, Event event, Locale locale) { + return createHomeRegattaLink("races", "racesOverviewLinkTitle", leaderboard, event, locale); + } + + private Pair createHomeLeaderboardLink(Leaderboard leaderboard, Event event, + Locale locale) { + return createHomeRegattaLink("leaderboard", "leaderboardShowResultsLinkTitle", leaderboard, event, locale); + } + + private Pair createHomeRegattaLink(String tab, String labelMessageKey, Leaderboard leaderboard, + Event event, Locale locale) { + String link = getBaseURL(event).toString() + "/gwt/Home.html?locale=" + locale.toLanguageTag() + + "#/regatta/" + tab + "/:eventId=" + event.getId() + "®attaId=" + leaderboard.getName(); + return new Pair(messages.get(locale, labelMessageKey), link); + } + + /** + * The base URL for notifications as extracted from the {@link Event#getBaseURL() event}; defaults + * to {@code https://www.sapsailing.com} if no base URL has been provided for the event. + */ + private URL getBaseURL(final Event event) { + final URL result; + if (event.getBaseURL() == null) { + result = defaultBaseURL; + } else { + result = event.getBaseURL(); + } + return result; + } + + @Override + public void notifyUserOnBoatClassRaceChangesStateToFinished(BoatClass boatClass, TrackedRace trackedRace, + Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) { + doWithEvent(leaderboard, (event, leaderboardGroup) -> { + mailQueue.addNotification(new NotificationSetNotification(boatClass, boatClassResults) { + + @Override + protected NotificationMailTemplate getMailTemplate(BoatClass objectToNotifyAbout, Locale locale) { + String raceDescription = calculateRaceDescription(locale, event, leaderboard, raceColumn, fleet); + return new NotificationMailTemplate( + messages.get(locale, "boatClassRaceFinishedSubject", boatClass.getDisplayName()), + messages.get(locale, "boatClassRaceFinishedBody", boatClass.getDisplayName(), + raceDescription), + getBaseURL(event), + createRaceBoardShowRaceLink(trackedRace, leaderboard, event, leaderboardGroup, locale), + createRaceBoardRaceAnalysisLink(trackedRace, leaderboard, event, leaderboardGroup, locale)); + } + }); + }); + } + + @Override + public void notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(BoatClass boatClass, Leaderboard leaderboard) { + // TODO don't send notifications when a notification for the same boatClass / leaderboard has already been sent shortly before + doWithEvent(leaderboard, (event, leaderboardGroup) -> { + + mailQueue.addNotification(new NotificationSetNotification(boatClass, boatClassResults) { + @Override + protected NotificationMailTemplate getMailTemplate(BoatClass objectToNotifyAbout, Locale locale) { + String leaderboardDescription = calculateLeaderboardDescription(locale, event, leaderboard); + return new NotificationMailTemplate( + messages.get(locale, "boatClassScoreCorrectionSubject", boatClass.getDisplayName()), + messages.get(locale, "boatClassScoreCorrectionBody", boatClass.getDisplayName(), + leaderboardDescription), + getBaseURL(event), + createHomeLeaderboardLink(leaderboard, event, locale)); + } + }); + }); + } + + public void notifyUserOnBoatClassUpcomingRace(BoatClass boatClass, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet, TimePoint when) { + doWithEvent(leaderboard, (event, leaderboardGroup) -> { + + mailQueue.addNotification(new NotificationSetNotification(boatClass, boatClassUpcomingRace) { + @Override + protected NotificationMailTemplate getMailTemplate(BoatClass objectToNotifyAbout, Locale locale) { + String raceDescription = calculateRaceDescription(locale, event, leaderboard, raceColumn, fleet); + String time = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG, locale) + .format(when.asDate()); + return new NotificationMailTemplate( + messages.get(locale, "boatClassUpcomingRaceSubject", boatClass.getDisplayName()), + messages.get(locale, "boatClassUpcomingRaceBody", boatClass.getDisplayName(), + raceDescription, time), + getBaseURL(event), + createHomeRacesListLink(leaderboard, event, locale)); + } + }); + }); + } + + @Override + public void notifyUserOnCompetitorPassesFinish(Competitor competitor, TrackedRace trackedRace, + Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) { + doWithEvent(leaderboard, (event, leaderboardGroup) -> { + + mailQueue.addNotification(new NotificationSetNotification(competitor, competitorResults) { + @Override + protected NotificationMailTemplate getMailTemplate(Competitor objectToNotifyAbout, Locale locale) { + String raceDescription = calculateRaceDescription(locale, event, leaderboard, raceColumn, fleet); + return new NotificationMailTemplate( + messages.get(locale, "competitorPassesFinishSubject", competitor.getName()), + messages.get(locale, "competitorPassesFinishBody", competitor.getName(), raceDescription), + getBaseURL(event), + createRaceBoardShowRaceLink(trackedRace, leaderboard, event, leaderboardGroup, locale), + createRaceBoardRaceAnalysisLink(trackedRace, leaderboard, event, leaderboardGroup, locale)); + } + }); + }); + } + + @Override + public void notifyUserOnCompetitorScoreCorrections(Competitor competitor, Leaderboard leaderboard) { + doWithEvent(leaderboard, (event, leaderboardGroup) -> { + + mailQueue.addNotification(new NotificationSetNotification(competitor, competitorResults) { + @Override + protected NotificationMailTemplate getMailTemplate(Competitor objectToNotifyAbout, Locale locale) { + String leaderboardDescription = calculateLeaderboardDescription(locale, event, leaderboard); + return new NotificationMailTemplate( + messages.get(locale, "competitorScoreCorrectionSubject", competitor.getName()), + messages.get(locale, "competitorScoreCorrectionBody", competitor.getName(), + leaderboardDescription), + getBaseURL(event), + createHomeLeaderboardLink(leaderboard, event, locale)); + } + }); + }); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java index 0b89f354a42..9d893ab042d 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java @@ -33,11 +33,13 @@ public class CreateEvent extends AbstractEventOperation { private final Iterable images; private final Iterable videos; private final URL officialWebsiteURL; + private final URL baseURL; private final Map sailorsInfoWebsiteURLs; private final Iterable leaderboardGroupIds; public CreateEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venue, - boolean isPublic, UUID id, URL officialWebsiteURL, Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos, Iterable leaderboardGroupIds) { + boolean isPublic, UUID id, URL officialWebsiteURL, URL baseURL, Map sailorsInfoWebsiteURLs, + Iterable images, Iterable videos, Iterable leaderboardGroupIds) { super(id); this.eventName = eventName; this.eventDescription = eventDescription; @@ -46,6 +48,7 @@ public class CreateEvent extends AbstractEventOperation { this.venue = venue; this.isPublic = isPublic; this.officialWebsiteURL = officialWebsiteURL; + this.baseURL = baseURL; this.sailorsInfoWebsiteURLs = sailorsInfoWebsiteURLs; this.images = images; this.videos = videos; @@ -71,7 +74,7 @@ public class CreateEvent extends AbstractEventOperation { @Override public Event internalApplyTo(RacingEventService toState) { final Event result = toState.createEventWithoutReplication(getEventName(), eventDescription, startDate, endDate, venue, isPublic, - getId(), officialWebsiteURL, sailorsInfoWebsiteURLs, images, videos); + getId(), officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, images, videos); List lgl = new ArrayList<>(); for (final UUID lgid : leaderboardGroupIds) { final LeaderboardGroup lg = toState.getLeaderboardGroupByID(lgid); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java index bd22cb972f2..4cabbe3c4a4 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java @@ -21,13 +21,14 @@ public class UpdateEvent extends AbstractEventOperation { private final String eventName; private final String eventDescription; private final URL officialWebsiteURL; + private final URL baseURL; private final Map sailorsInfoWebsiteURLs; private final Iterable images; private final Iterable videos; public UpdateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, - String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, Map sailorsInfoWebsiteURLs, - Iterable images, Iterable videos) { + String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL baseURL, + Map sailorsInfoWebsiteURLs, Iterable images, Iterable videos) { super(id); this.eventName = eventName; this.eventDescription = eventDescription; @@ -37,6 +38,7 @@ public class UpdateEvent extends AbstractEventOperation { this.isPublic = isPublic; this.leaderboardGroupIds = leaderboardGroupIds; this.officialWebsiteURL = officialWebsiteURL; + this.baseURL = baseURL; this.sailorsInfoWebsiteURLs = sailorsInfoWebsiteURLs; this.images = images; this.videos = videos; @@ -57,7 +59,7 @@ public class UpdateEvent extends AbstractEventOperation { @Override public Void internalApplyTo(RacingEventService toState) { toState.updateEvent(getId(), eventName, eventDescription, startDate, endDate, venueName, isPublic, - leaderboardGroupIds, officialWebsiteURL, sailorsInfoWebsiteURLs, images, videos); + leaderboardGroupIds, officialWebsiteURL, baseURL, sailorsInfoWebsiteURLs, images, videos); return null; } } diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index cdb66bcaf44..729a3780e26 100644 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -52,6 +52,12 @@ Leave the maximum empty to not provide any maximum. Setting to 0 will cause no scores to be discarded in that series.
  • +
  • An event now has a "Base URL" which should be used to enable useful e-mail notifications with + links embedded in them that lead to the correct server. If a notification is to contain + a link to a dedicated event server, the event cannot be reached through www.sapsailing.com, + and so if this generic link is used as a default, users will end up not finding the page + linked to by the notification. +
  • June 2016

    @@ -138,6 +144,11 @@ No sailors info link is shown for UI languages if there is neither a default link nor one for the specific locale, even if there is one for another locale configured. +
  • The mechanism to handle RegattaOverview settings has been changed to a more generic approach. + This leads to slightly changed URL parameter names so that old bookmarks could be broken in a way + so that the actual shown set of races is different using the new version because the old parameters aren't correctly recognized. + All automatically generated links to this page in Home and AdminConsole have been updated to use the new parameter names. +
  • February 2016

    diff --git a/java/com.sap.sse.common/META-INF/MANIFEST.MF b/java/com.sap.sse.common/META-INF/MANIFEST.MF index d8f1e10697a..290b7f09b55 100644 --- a/java/com.sap.sse.common/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.common/META-INF/MANIFEST.MF @@ -13,7 +13,11 @@ Export-Package: com.sap.sse.common, com.sap.sse.common.scalablevalue, com.sap.sse.common.search, com.sap.sse.common.settings, + com.sap.sse.common.settings.generic, + com.sap.sse.common.settings.generic.base, + com.sap.sse.common.settings.generic.converter, + com.sap.sse.common.settings.serializer, + com.sap.sse.common.settings.value, com.sap.sse.common.util -Require-Bundle: org.json.simple;bundle-version="1.1.0", - com.sap.sse.datamining.annotations +Require-Bundle: com.sap.sse.datamining.annotations Bundle-ActivationPolicy: lazy diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Stoppable.java b/java/com.sap.sse.common/src/com/sap/sse/common/Stoppable.java new file mode 100644 index 00000000000..5b798071abe --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Stoppable.java @@ -0,0 +1,11 @@ +package com.sap.sse.common; + +/** + * Common definition for classes that hold resources that must be explicitly cleaned up. + * + * This can be used to e.g. recognize all {@link Stoppable} instance in a {@link org.osgi.framework.BundleActivator}. + * This makes it possible to cleanup all {@link Stoppable} instances in a loop to make the code much shorter. + */ +public interface Stoppable { + void stop(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java index bc4b3ddb3aa..76837ee6bcc 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java @@ -359,10 +359,25 @@ public class Util { } public static void addToValueSet(Map> map, K key, V value) { - if (! map.containsKey(key)) { - map.put(key, new HashSet()); + Set set = map.get(key); + if (set == null) { + set = new HashSet(); + map.put(key, set); + } + set.add(value); + } + + public static void removeFromAllValueSets(Map> map, V value) { + for (Set set : map.values()) { + set.remove(value); + } + } + + public static void removeFromValueSet(Map> map, K key, V value) { + final Set valuesPerKey = map.get(key); + if(valuesPerKey != null) { + valuesPerKey.remove(value); } - map.get(key).add(value); } public static String join(String separator, String... strings) { @@ -483,4 +498,10 @@ public class Util { } return result; } + + public static List asList(Iterable visibleCourseAreas) { + ArrayList list = new ArrayList(); + addAll(visibleCourseAreas, list); + return list; + } } \ No newline at end of file diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/AbstractSettings.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/AbstractSettings.java old mode 100755 new mode 100644 index 12a815e7385..6a664a19e0d --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/AbstractSettings.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/AbstractSettings.java @@ -1,28 +1,15 @@ -package com.sap.sse.common.settings; - -import java.util.Collections; -import java.util.Map; - -public abstract class AbstractSettings implements Settings { - @Override - public SettingType getType() { - return SettingType.MAP; - } - - /** - * This default implementation returns an empty map. - */ - @Override - public Map getNonDefaultSettings() { - return Collections.emptyMap(); - } - - public boolean equals(Object other) { - return getNonDefaultSettings().equals(((Settings) other).getNonDefaultSettings()); - } - - @Override - public String toString() { - return getNonDefaultSettings().toString(); - } -} +package com.sap.sse.common.settings; + +/** + * Common superclass for custom Settings used by Components. This kind of Settings is not serializable at all. There are + * more specific super classes for different serialization needs: + *
      + *
    • {@link SerializableSettings} can be used for {@link Settings} that need to be Java-serializable and/or + * GWT-RPC-compatible
    • + *
    • {@link com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings} support a wide variety of serialization mechanisms by using a generic + * mechanism to declare the settings that requires that implementers comply to some specific rules
    • + *
    + * + */ +public abstract class AbstractSettings implements Settings { +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/GwtIncompatible.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/GwtIncompatible.java new file mode 100644 index 00000000000..5d2d0268bca --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/GwtIncompatible.java @@ -0,0 +1,27 @@ +package com.sap.sse.common.settings; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Private version of com.google.gwt.core.shared.GwtIncompatible. + * GWT doesn't check for the concrete annotation class but only checks if there is an annotation named GwtIncompatible. + * With this private annotation we can flag something as GWT incompatible without introducing a dependency to GWT core. + * + */ +@Retention(RetentionPolicy.CLASS) +@Target({ + ElementType.TYPE, ElementType.METHOD, + ElementType.CONSTRUCTOR, ElementType.FIELD }) +@Documented +public @interface GwtIncompatible { + /** + * An attribute that can be used to explain why the code is incompatible. + * A GwtIncompatible annotation can have any number of attributes; attributes + * are for documentation purposes and are ignored by the GWT compiler. + */ + String value() default ""; +} \ No newline at end of file diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/ListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/ListSetting.java deleted file mode 100755 index aedafdc2a62..00000000000 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/ListSetting.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.sap.sse.common.settings; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -public class ListSetting implements Iterable, Setting { - private final Iterable list; - - public ListSetting(Iterable list) { - super(); - this.list = list; - } - - @Override - public Iterator iterator() { - return list.iterator(); - } - - /** - * Precondition: this list holds {@link StringSetting}s. Otherwise, a {@link ClassCastException} will be thrown. - */ - public > List> getEnumList(Class enumClass) { - final List> result = new ArrayList<>(); - for (T t : this) { - result.add(((StringSetting) t).getEnum(enumClass)); - } - return result; - } - - @Override - public SettingType getType() { - return SettingType.LIST; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((list == null) ? 0 : list.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - ListSetting other = (ListSetting) obj; - if (list == null) { - if (other.list != null) - return false; - } else if (!list.equals(other.list)) - return false; - return true; - } - - @Override - public String toString() { - return list.toString(); - } -} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/MapSettings.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/MapSettings.java deleted file mode 100755 index 5fab975bac5..00000000000 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/MapSettings.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sap.sse.common.settings; - -import java.util.Map; - -public class MapSettings extends AbstractSettings { - private final Map settingsMap; - - public MapSettings(Map settingsMap) { - this.settingsMap = settingsMap; - } - - @Override - public Map getNonDefaultSettings() { - return settingsMap; - } -} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/NumberSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/NumberSetting.java deleted file mode 100755 index e0fa2c42f1e..00000000000 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/NumberSetting.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.sap.sse.common.settings; - -import java.math.BigDecimal; -import java.math.BigInteger; - -public class NumberSetting implements Setting { - private final BigDecimal number; - - public NumberSetting(BigDecimal number) { - this.number = number; - } - - public NumberSetting(double number) { - this.number = new BigDecimal(number); - } - - public NumberSetting(long number) { - this.number = new BigDecimal(number); - } - - public NumberSetting(int number) { - this.number = new BigDecimal(number); - } - - public NumberSetting(BigInteger number) { - this.number = new BigDecimal(number); - } - - public NumberSetting(Number obj) { - if (obj instanceof BigDecimal) { - this.number = (BigDecimal) obj; - } else if (obj instanceof BigInteger) { - this.number = new BigDecimal((BigInteger) obj); - } else if (obj instanceof Double || obj instanceof Float) { - this.number = new BigDecimal(((Number) obj).doubleValue()); - } else { - this.number = new BigDecimal(obj.longValue()); - } - } - - public BigDecimal getNumber() { - return number; - } - - @Override - public SettingType getType() { - return SettingType.NUMBER; - } - - @Override - public String toString() { - return ""+number; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((number == null) ? 0 : number.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - NumberSetting other = (NumberSetting) obj; - if (number == null) { - if (other.number != null) - return false; - } else if (!number.equals(other.number)) - return false; - return true; - } -} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/SerializableSettings.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/SerializableSettings.java index ae32a9aab7b..d46dd54608a 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/SerializableSettings.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/SerializableSettings.java @@ -2,8 +2,9 @@ package com.sap.sse.common.settings; import java.io.Serializable; +/** + * Super class for {@link Settings} that need to be Java-serializable or compatible with GWT-RPC. + */ public abstract class SerializableSettings extends AbstractSettings implements Serializable { - private static final long serialVersionUID = -2710627501473421333L; - } diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/Setting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/Setting.java deleted file mode 100755 index 27aad725a2f..00000000000 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/Setting.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sap.sse.common.settings; - -/** - * A setting as contained as values in the map returned by {@link AbstractSettings#getSettings()}. This generic - * view on a {@link AbstractSettings} object can then be used by one or more serializer / de-serializer combos - * for different serialized formats such as URLs, human-readable strings, obfuscated strings, JSON, or - * a binary format. - * - * @author Axel Uhl (D043530) - * - */ -public interface Setting { - SettingType getType(); -} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/SettingType.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/SettingType.java deleted file mode 100755 index 38f61391f86..00000000000 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/SettingType.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sap.sse.common.settings; - -public enum SettingType { - STRING, NUMBER, LIST, ENUM, MAP; -} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/Settings.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/Settings.java old mode 100755 new mode 100644 index 9419ffea7c5..b73e306c612 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/Settings.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/Settings.java @@ -1,32 +1,14 @@ package com.sap.sse.common.settings; -import java.util.Map; - /** - * A generic format to keep the non-default settings for a Component. Such settings may be initialized with - * defaults. Usually they can be edited by a user using a SettingsDialogComponent displayed by a + * A generic format to keep the settings for a Component. Such settings may be initialized with defaults. + * Usually they can be edited by a user using a SettingsDialogComponent displayed by a * SettingsDialog. The interface is homed in this server-side, shared bundle because settings may as well * be handled on the server side, e.g., in order to store them in the user store or to apply them to a server-side * component. - *

    - * - * Settings need to be serialized and de-serialized to/from different formats. In a URL they should try to follow - * reasonable URL parameter encoding conventions and be human readable/editable. They may also be stored in a cookie or - * in the user-specific preferences store where JSON may be a useful format to keep them as a {@link String}. - *

    - * - * In order to enable mapping the settings to URL parameters, each setting needs to have a name and a {@link Setting} - * value that in turn can be serialized accordingly. Furthermore, the component to which the settings apply needs to - * provide a String identifier. This way, generic serializers/de-serializers can take over the serialization from/to the - * various formats. * * @author Axel Uhl (D043530) * */ -public interface Settings extends Setting { - /** - * Obtains all settings that have non-default values and therefore are required for the later re-construction - * of the settings object when passed the resulting map. - */ - Map getNonDefaultSettings(); +public interface Settings { } diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/StringSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/StringSetting.java deleted file mode 100755 index 9d5dfc5af93..00000000000 --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/StringSetting.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.sap.sse.common.settings; - -public class StringSetting implements Setting { - private final String string; - - public StringSetting(String string) { - this.string = string; - } - - public StringSetting(Enum literal) { - this.string = literal.name(); - } - - public String getString() { - return string; - } - - public > Enum getEnum(Class enumClass) { - Enum value = null; - if (getString() != null) { - boolean found = false; - for (Enum literal : enumClass.getEnumConstants()) { - if (literal.name().equals(getString())) { - value = literal; - found = true; - break; - } - } - if (!found) { - throw new IllegalArgumentException("Couldn't find literal "+getString()+" in enum "+enumClass.getName()); - } - } - return value; - } - - @Override - public SettingType getType() { - return SettingType.STRING; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((string == null) ? 0 : string.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - StringSetting other = (StringSetting) obj; - if (string == null) { - if (other.string != null) - return false; - } else if (!string.equals(other.string)) - return false; - return true; - } - - @Override - public String toString() { - return "\""+string+"\""; - } -} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/AbstractGenericSerializableSettings.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/AbstractGenericSerializableSettings.java new file mode 100644 index 00000000000..fd3abc36c76 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/AbstractGenericSerializableSettings.java @@ -0,0 +1,240 @@ +package com.sap.sse.common.settings.generic; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.sap.sse.common.settings.GwtIncompatible; +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.value.SettingsValue; +import com.sap.sse.common.settings.value.Value; + +/** + * Common superclass for custom Settings used by Components. The following example shows how to implement custom + * Settings: + * + *

    + * 
    + * public final class ExampleSettings extends AbstractGenericSerializableSettings {
    + *     private transient StringSetting myString;
    + *     private transient BooleanSetting myBoolean;
    + *     
    + *     // Used to create instances for a component
    + *     public ExampleSettings() {
    + *     }
    + *     
    + *     // Used to create nested settings
    + *     public ExampleSettings(String name, AbstractSettings parentSettings) {
    + *         super(name, parentSettings);
    + *     }
    + *     
    + *     @Override
    + *     protected void addChildSettings() {
    + *         myString = new UUIDSetting("myString", this)
    + *         myBoolean = new BooleanSetting("myBoolean", this);
    + *     }
    + *     
    + *     public String getMyString() {
    + *         return this.myString.getValue();
    + *     }
    + *     
    + *     public void setMyString(String myString) {
    + *         return this.myString.setValue(myString);
    + *     }
    + *     
    + *     public boolean isMyBoolean() {
    + *         return this.myBoolean.getValue();
    + *     }
    + *     
    + *     public void setMyBoolean(boolean myBoolean) {
    + *         return this.myBoolean.setValue(myBoolean);
    + *     }
    + * }
    + * 
    + * 
    + * + * To correctly support several kinds of Serialization (Java, GWT and Custom), you need to ensure the following: + *
      + *
    • the Settings class must have a default constructor
    • + *
    • child settings fields need to be transient
    • + *
    • child settings instances must be initialized in {@link #addChildSettings()} method
    • + *
    + * + * If a custom Settings class only has {@link Setting} oder {@link Settings} children that are correctly attached by + * calling their constructor with a name and the parent {@link Settings} you do not need to implement + * {@link #hashCode()}, {@link #equals(Object)} and {@link #toString()}. There are generic implementations of those methods that are + * based on the child {@link Setting}s which should be sufficient for most cases. + * + */ +public abstract class AbstractGenericSerializableSettings extends AbstractSetting implements GenericSerializableSettings { + private static final long serialVersionUID = -415371632804540785L; + private SettingsValue value; + private transient Map childSettings; + + /** + * Default constructor for direct instantiation of root settings objects. + */ + public AbstractGenericSerializableSettings() { + value = new SettingsValue(); + addChildSettingsInternal(); + } + + /** + * Constructor for automatic attachment of a child settings object to its parent settings object. + * + * @param name the name of the child setting + * @param settings the parent settings to attach this settings object to + */ + public AbstractGenericSerializableSettings(String name, AbstractGenericSerializableSettings settings) { + super(name, settings); + value = (SettingsValue) settings.getValue(name); + if(value == null) { + value = new SettingsValue(); + settings.setValue(name, value); + } + addChildSettingsInternal(); + } + + /** + * Internal use only for correct lifecycle of {@link SettingsList}. + */ + protected void adoptValue(SettingsValue value) { + this.value = value; + if(childSettings != null) { + for(Map.Entry entry : childSettings.entrySet()) { + Setting childSetting = entry.getValue(); + if(childSetting instanceof AbstractGenericSerializableSettings) { + Value childValue = value.getValue(entry.getKey()); + if(childValue != null) { + ((AbstractGenericSerializableSettings) childSetting).adoptValue((SettingsValue) childValue); + } + } + if(childSetting instanceof SettingsList) { + Value childValue = value.getValue(entry.getKey()); + if(childValue != null) { + ((SettingsList) childSetting).adoptValue(); + } + } + } + } + } + + protected SettingsValue getInnerValueObject() { + return value; + } + + protected final void addChildSettingsInternal() { + if(childSettings == null) { + childSettings = new HashMap<>(); + addChildSettings(); + } + } + + /** + * Overwrite this method to initialize child settings. + * {@link AbstractGenericSerializableSettings} for an example. + * + * TODO make abstract when all Settings are ported to the new system + * + */ + protected void addChildSettings() { + } + + // TODO make protected + public Value getValue(String settingName) { + return value.getValue(settingName); + } + + // TODO make protected + public void setValue(String settingName, Value value) { + this.value.setValue(settingName, value); + } + + protected void addSetting(String name, Setting setting) { + if (name.contains(GenericSerializableSettings.PATH_SEPARATOR)) { + throw new IllegalArgumentException("\"" + GenericSerializableSettings.PATH_SEPARATOR + + "\" is currently not allowed for setting names"); + } + if (childSettings.containsKey(name)) { + throw new IllegalArgumentException("setting with name " + name + " already added to setting type " + + getClass().getSimpleName()); + } + childSettings.put(name, setting); + } + + @Override + public boolean isDefaultValue() { + for (Setting setting : childSettings.values()) { + if (!setting.isDefaultValue()) { + return false; + } + } + return true; + } + + @Override + public void resetToDefault() { + for (Setting setting : childSettings.values()) { + setting.resetToDefault(); + } + } + + @Override + public Map getChildSettings() { + return Collections.unmodifiableMap(childSettings); + } + + @GwtIncompatible + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + addChildSettingsInternal(); + } + + @GwtIncompatible + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + public String toString() { + StringBuilder sb = new StringBuilder("{(").append(this.getClass().getSimpleName()).append(")"); + boolean first = true; + for (Map.Entry childSetting : childSettings.entrySet()) { + if (!first) { + sb.append("; "); + } + sb.append(childSetting.getKey() + "=" + childSetting.getValue()); + + first = false; + } + sb.append("}"); + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((childSettings == null) ? 0 : childSettings.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractGenericSerializableSettings other = (AbstractGenericSerializableSettings) obj; + if (childSettings == null) { + if (other.childSettings != null) + return false; + } else if (!childSettings.equals(other.childSettings)) + return false; + return true; + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/AbstractSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/AbstractSetting.java new file mode 100644 index 00000000000..b83b7849b36 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/AbstractSetting.java @@ -0,0 +1,21 @@ +package com.sap.sse.common.settings.generic; + + + +public abstract class AbstractSetting implements Setting { + + protected transient final AbstractGenericSerializableSettings settings; + protected transient final String settingName; + + public AbstractSetting(String settingName, AbstractGenericSerializableSettings settings) { + this.settingName = settingName; + this.settings = settings; + if(settings != null) { + settings.addSetting(settingName, this); + } + } + + public AbstractSetting() { + this(null, null); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/BooleanSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/BooleanSetting.java new file mode 100644 index 00000000000..7e77c253dc8 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/BooleanSetting.java @@ -0,0 +1,15 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.generic.converter.BooleanConverter; + +public class BooleanSetting extends AbstractValueSetting { + + public BooleanSetting(String name, AbstractGenericSerializableSettings settings) { + this(name, settings, null); + } + + public BooleanSetting(String name, AbstractGenericSerializableSettings settings, Boolean defaultValue) { + super(name, settings, defaultValue, BooleanConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/CollectionSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/CollectionSetting.java new file mode 100644 index 00000000000..ba3e40c4348 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/CollectionSetting.java @@ -0,0 +1,10 @@ +package com.sap.sse.common.settings.generic; + + +public interface CollectionSetting extends Setting { + + void setValues(Iterable values); + + Iterable getValues(); + +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DecimalListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DecimalListSetting.java new file mode 100644 index 00000000000..cceaf14770c --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DecimalListSetting.java @@ -0,0 +1,12 @@ +package com.sap.sse.common.settings.generic; + +import java.math.BigDecimal; + +import com.sap.sse.common.settings.generic.base.AbstractValueListSetting; +import com.sap.sse.common.settings.generic.converter.DecimalConverter; + +public class DecimalListSetting extends AbstractValueListSetting { + public DecimalListSetting(String name, AbstractGenericSerializableSettings settings) { + super(name, settings, DecimalConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DecimalSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DecimalSetting.java new file mode 100644 index 00000000000..b436699a243 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DecimalSetting.java @@ -0,0 +1,17 @@ +package com.sap.sse.common.settings.generic; + +import java.math.BigDecimal; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.generic.converter.DecimalConverter; + +public class DecimalSetting extends AbstractValueSetting { + + public DecimalSetting(String name, AbstractGenericSerializableSettings settings) { + this(name, settings, null); + } + + public DecimalSetting(String name, AbstractGenericSerializableSettings settings, BigDecimal defaultValue) { + super(name, settings, defaultValue, DecimalConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DoubleListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DoubleListSetting.java new file mode 100644 index 00000000000..5ae268f8e0d --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DoubleListSetting.java @@ -0,0 +1,10 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueListSetting; +import com.sap.sse.common.settings.generic.converter.DoubleConverter; + +public class DoubleListSetting extends AbstractValueListSetting { + public DoubleListSetting(String name, AbstractGenericSerializableSettings settings) { + super(name, settings, DoubleConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DoubleSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DoubleSetting.java new file mode 100644 index 00000000000..da9b39cbfc5 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/DoubleSetting.java @@ -0,0 +1,15 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.generic.converter.DoubleConverter; + +public class DoubleSetting extends AbstractValueSetting { + + public DoubleSetting(String name, AbstractGenericSerializableSettings settings) { + this(name, settings, null); + } + + public DoubleSetting(String name, AbstractGenericSerializableSettings settings, Double defaultValue) { + super(name, settings, defaultValue, DoubleConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/EnumListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/EnumListSetting.java new file mode 100644 index 00000000000..d014be853b9 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/EnumListSetting.java @@ -0,0 +1,10 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueListSetting; +import com.sap.sse.common.settings.generic.converter.EnumConverter; + +public class EnumListSetting> extends AbstractValueListSetting { + public EnumListSetting(String name, AbstractGenericSerializableSettings settings, StringToEnumConverter stringToEnumConverter) { + super(name, settings, new EnumConverter<>(stringToEnumConverter)); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/EnumSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/EnumSetting.java new file mode 100644 index 00000000000..d54cf953a89 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/EnumSetting.java @@ -0,0 +1,15 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.generic.converter.EnumConverter; + +public class EnumSetting> extends AbstractValueSetting { + + public EnumSetting(String name, AbstractGenericSerializableSettings settings, StringToEnumConverter stringToEnumConverter) { + this(name, settings, null, stringToEnumConverter); + } + + public EnumSetting(String name, AbstractGenericSerializableSettings settings, T defaultValue, StringToEnumConverter stringToEnumConverter) { + super(name, settings, defaultValue, new EnumConverter<>(stringToEnumConverter)); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/GenericSerializableSettings.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/GenericSerializableSettings.java new file mode 100644 index 00000000000..c7776666cbd --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/GenericSerializableSettings.java @@ -0,0 +1,46 @@ +package com.sap.sse.common.settings.generic; + +import java.io.Serializable; +import java.util.Map; + +import com.sap.sse.common.settings.Settings; + +/** + * {@link Settings} that need to be serialized and de-serialized to/from different formats. In a URL they should try to follow + * reasonable URL parameter encoding conventions and be human readable/editable. They may also be stored in a cookie or + * in the user-specific preferences store where JSON may be a useful format to keep them as a {@link String}. + *

    + * + * The following serializers are available: + *

      + *
    • + * {@link com.sap.sse.common.settings.serializer.SettingsToStringMapSerializer}: Serializes to a Map containing String + * keys and values
    • + *
    • + * {@link com.sap.sse.gwt.settings.SettingsToUrlSerializer}: Serializes to a GWT UrlBuilder
    • + *
    • + * {@link com.sap.sse.shared.settings.SettingsToJsonSerializer}: Serializes to org.json.simple objects usable in the + * backend and on Android
    • + *
    • + * {@link com.sap.sse.gwt.settings.SettingsToJsonSerializerGWT}: Serializes to GWT's JSON types. The resulting JSON + * String is compatible with the backend-side SettingsToJsonSerializer.
    • + *
    + * + * In order to enable mapping the settings to URL parameters or JSON structures, each setting needs an associated name + * and the {@link Settings} hierarchy must be ensured. For this every {@link Setting} has a constructor requiring it's + * name and parent Settings object. To make {@link Settings} able to be nested in other {@link Settings} you need to + * provide the very same constructor that can simply call super on {@link AbstractSettings}. This way, generic + * serializers/de-serializers can take over the serialization from/to the various formats. + */ +public interface GenericSerializableSettings extends Settings, Setting, Serializable { + + /** + * Path separator for serializers that construct flat keys for the hierarchical settings structures (e.g. URL). + */ + public static final String PATH_SEPARATOR = "."; + + /** + * Obtains all child settings. + */ + Map getChildSettings(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/HasSettingsFactory.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/HasSettingsFactory.java new file mode 100644 index 00000000000..d1c37c38d2a --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/HasSettingsFactory.java @@ -0,0 +1,7 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.Settings; + +public interface HasSettingsFactory { + SettingsFactory getSettingsFactory(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/HasValueSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/HasValueSetting.java new file mode 100644 index 00000000000..e375bb47b76 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/HasValueSetting.java @@ -0,0 +1,5 @@ +package com.sap.sse.common.settings.generic; + +public interface HasValueSetting extends Setting { + ValueConverter getValueConverter(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/Setting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/Setting.java new file mode 100644 index 00000000000..10f57113dba --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/Setting.java @@ -0,0 +1,16 @@ +package com.sap.sse.common.settings.generic; + +/** + * A setting as contained as values in the map returned by {@link GenericSerializableSettings#getSettings()}. This generic + * view on a {@link GenericSerializableSettings} object can then be used by one or more serializer / de-serializer combos + * for different serialized formats such as URLs, human-readable strings, obfuscated strings, JSON, or + * a binary format. + * + * @author Axel Uhl (D043530) + * + */ +public interface Setting { + boolean isDefaultValue(); + + void resetToDefault(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsFactory.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsFactory.java new file mode 100644 index 00000000000..86f151238db --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsFactory.java @@ -0,0 +1,14 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.Settings; + +/** + * + * Used by {@link SettingsList} for deserialization of complex {@link Settings} based collections. + * Because GWT can't use reflection the serialization mechanism must rely on factories to create nested instances on demand. + * + * @param The type of the created {@link Settings} objects + */ +public interface SettingsFactory { + T newInstance(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsList.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsList.java new file mode 100644 index 00000000000..6933b8ea0e2 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsList.java @@ -0,0 +1,121 @@ +package com.sap.sse.common.settings.generic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.sap.sse.common.settings.value.SettingsValue; +import com.sap.sse.common.settings.value.Value; +import com.sap.sse.common.settings.value.ValueCollectionValue; +import com.sap.sse.common.settings.value.ValueListValue; + + +public class SettingsList extends AbstractSetting implements SettingsListSetting { + + private SettingsFactory settingsFactory; + private final List values = new ArrayList<>(); + + public SettingsList(String name, AbstractGenericSerializableSettings settings, SettingsFactory settingsFactory) { + super(name, settings); + this.settingsFactory = settingsFactory; + adoptValue(); + } + + protected void adoptValue() { + values.clear(); + ValueListValue value = getValue(); + if(value != null) { + for(Value val : value.getValueObjects()) { + T childSettingsInstance = settingsFactory.newInstance(); + childSettingsInstance.adoptValue((SettingsValue)val); + values.add(childSettingsInstance); + } + } + } + + @Override + public SettingsFactory getSettingsFactory() { + return settingsFactory; + } + + private ValueCollectionValue> ensureValue() { + ValueListValue result = getValue(); + if(result == null) { + result = new ValueListValue(); + settings.setValue(settingName, result); + } + return result; + } + + private ValueListValue getValue() { + return (ValueListValue) settings.getValue(settingName); + } + + @Override + public String toString() { + return getValues().toString(); + } + + @Override + public boolean isDefaultValue() { + // explicit default values are possible to implement + // currently, empty is always default + return values.isEmpty(); + } + + @Override + public void resetToDefault() { + this.values.clear(); + settings.setValue(settingName, null); + } + + @Override + public Iterable getValues() { + return Collections.unmodifiableCollection(values); + } + + @Override + public void setValues(Iterable values) { + this.values.clear(); + ValueCollectionValue> valueObject = ensureValue(); + valueObject.clear(); + if(values != null) { + for(T value : values) { + this.values.add(value); + valueObject.addValue(value.getInnerValueObject()); + } + } + } + + public void addValue(T value) { + ValueCollectionValue> valueObject = ensureValue(); + this.values.add(value); + valueObject.addValue(value.getInnerValueObject()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((values == null) ? 0 : values.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + SettingsList other = (SettingsList) obj; + if (values == null) { + if (other.values != null) + return false; + } else if (!values.equals(other.values)) + return false; + return true; + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsListSetting.java new file mode 100644 index 00000000000..72eece7f9b9 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/SettingsListSetting.java @@ -0,0 +1,4 @@ +package com.sap.sse.common.settings.generic; + +public interface SettingsListSetting extends CollectionSetting, HasSettingsFactory { +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringListSetting.java new file mode 100644 index 00000000000..74f7ed6615a --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringListSetting.java @@ -0,0 +1,10 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueListSetting; +import com.sap.sse.common.settings.generic.converter.StringConverter; + +public class StringListSetting extends AbstractValueListSetting { + public StringListSetting(String name, AbstractGenericSerializableSettings settings) { + super(name, settings, StringConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringSetSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringSetSetting.java new file mode 100644 index 00000000000..9b4cb237a3e --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringSetSetting.java @@ -0,0 +1,22 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetSetting; +import com.sap.sse.common.settings.generic.converter.StringConverter; + +public class StringSetSetting extends AbstractValueSetSetting { + public StringSetSetting(String name, AbstractGenericSerializableSettings settings) { + super(name, settings, StringConverter.INSTANCE); + } + + public StringSetSetting(String name, AbstractGenericSerializableSettings settings, boolean emptyIsDefault) { + super(name, settings, emptyIsDefault, StringConverter.INSTANCE); + } + + public StringSetSetting(String name, AbstractGenericSerializableSettings settings, Iterable defaultValues) { + super(name, settings, defaultValues, StringConverter.INSTANCE); + } + + public StringSetSetting(String name, AbstractGenericSerializableSettings settings, Iterable defaultValues, boolean emptyIsDefault) { + super(name, settings, defaultValues, emptyIsDefault, StringConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringSetting.java new file mode 100644 index 00000000000..b1536b19b32 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringSetting.java @@ -0,0 +1,15 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.generic.converter.StringConverter; + +public class StringSetting extends AbstractValueSetting { + + public StringSetting(String name, AbstractGenericSerializableSettings settings) { + this(name, settings, null); + } + + public StringSetting(String name, AbstractGenericSerializableSettings settings, String defaultValue) { + super(name, settings, defaultValue, StringConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringToEnumConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringToEnumConverter.java new file mode 100644 index 00000000000..83447be94ae --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/StringToEnumConverter.java @@ -0,0 +1,13 @@ +package com.sap.sse.common.settings.generic; + +/** + * GWT can't use reflection. When deserializing Settings that contain {@link Enum} values there must be come conversion + * of raw String values to the actual enum constants. So an instance of {@link StringToEnumConverter} must be provided + * for every {@link Enum} based {@link Setting} objects. + * + * @param + * Type of the created enum instances + */ +public interface StringToEnumConverter> { + T fromString(String stringValue); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDListSetting.java new file mode 100644 index 00000000000..3b24a916cb7 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDListSetting.java @@ -0,0 +1,12 @@ +package com.sap.sse.common.settings.generic; + +import java.util.UUID; + +import com.sap.sse.common.settings.generic.base.AbstractValueListSetting; +import com.sap.sse.common.settings.generic.converter.UUIDConverter; + +public class UUIDListSetting extends AbstractValueListSetting { + public UUIDListSetting(String name, AbstractGenericSerializableSettings settings) { + super(name, settings, UUIDConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDSetSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDSetSetting.java new file mode 100644 index 00000000000..704a60268d7 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDSetSetting.java @@ -0,0 +1,24 @@ +package com.sap.sse.common.settings.generic; + +import java.util.UUID; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetSetting; +import com.sap.sse.common.settings.generic.converter.UUIDConverter; + +public class UUIDSetSetting extends AbstractValueSetSetting { + public UUIDSetSetting(String name, AbstractGenericSerializableSettings settings) { + super(name, settings, UUIDConverter.INSTANCE); + } + + public UUIDSetSetting(String name, AbstractGenericSerializableSettings settings, boolean emptyIsDefault) { + super(name, settings, emptyIsDefault, UUIDConverter.INSTANCE); + } + + public UUIDSetSetting(String name, AbstractGenericSerializableSettings settings, Iterable defaultValues) { + super(name, settings, defaultValues, UUIDConverter.INSTANCE); + } + + public UUIDSetSetting(String name, AbstractGenericSerializableSettings settings, Iterable defaultValues, boolean emptyIsDefault) { + super(name, settings, defaultValues, emptyIsDefault, UUIDConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDSetting.java new file mode 100644 index 00000000000..1871d841d5d --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/UUIDSetting.java @@ -0,0 +1,17 @@ +package com.sap.sse.common.settings.generic; + +import java.util.UUID; + +import com.sap.sse.common.settings.generic.base.AbstractValueSetting; +import com.sap.sse.common.settings.generic.converter.UUIDConverter; + +public class UUIDSetting extends AbstractValueSetting { + + public UUIDSetting(String name, AbstractGenericSerializableSettings settings) { + this(name, settings, null); + } + + public UUIDSetting(String name, AbstractGenericSerializableSettings settings, UUID defaultValue) { + super(name, settings, defaultValue, UUIDConverter.INSTANCE); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueCollectionSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueCollectionSetting.java new file mode 100644 index 00000000000..b71750cdbb4 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueCollectionSetting.java @@ -0,0 +1,5 @@ +package com.sap.sse.common.settings.generic; + + +public interface ValueCollectionSetting extends HasValueSetting, CollectionSetting { +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueConverter.java new file mode 100644 index 00000000000..d9a3de5f24a --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueConverter.java @@ -0,0 +1,18 @@ +package com.sap.sse.common.settings.generic; + +import com.sap.sse.common.settings.value.Value; + +public interface ValueConverter { + + Object toJSONValue(T value); + + T fromJSONValue(Object jsonValue); + + String toStringValue(T value); + + T fromStringValue(String stringValue); + + Value toValue(T value); + + T fromValue(Value value); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueSetSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueSetSetting.java new file mode 100644 index 00000000000..a7d715e1ca7 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueSetSetting.java @@ -0,0 +1,5 @@ +package com.sap.sse.common.settings.generic; + +public interface ValueSetSetting extends ValueCollectionSetting { + void setDefaultValues(Iterable defaultValues); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueSetting.java new file mode 100644 index 00000000000..16644bf880f --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/ValueSetting.java @@ -0,0 +1,7 @@ +package com.sap.sse.common.settings.generic; + +public interface ValueSetting extends HasValueSetting { + T getValue(); + + void setValue(T value); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractHasValueSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractHasValueSetting.java new file mode 100644 index 00000000000..1c8f26e8e20 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractHasValueSetting.java @@ -0,0 +1,21 @@ +package com.sap.sse.common.settings.generic.base; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.AbstractSetting; +import com.sap.sse.common.settings.generic.HasValueSetting; +import com.sap.sse.common.settings.generic.ValueConverter; + +public abstract class AbstractHasValueSetting extends AbstractSetting implements HasValueSetting { + + private ValueConverter valueConverter; + + public AbstractHasValueSetting(String name, AbstractGenericSerializableSettings settings, ValueConverter valueConverter) { + super(name, settings); + this.valueConverter = valueConverter; + } + + @Override + public ValueConverter getValueConverter() { + return valueConverter; + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueCollectionSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueCollectionSetting.java new file mode 100644 index 00000000000..146364a5b6f --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueCollectionSetting.java @@ -0,0 +1,93 @@ +package com.sap.sse.common.settings.generic.base; + +import java.util.Collection; +import java.util.Collections; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.ValueCollectionSetting; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.Value; +import com.sap.sse.common.settings.value.ValueCollectionValue; + +public abstract class AbstractValueCollectionSetting> extends AbstractHasValueSetting implements ValueCollectionSetting { + + public AbstractValueCollectionSetting(String name, AbstractGenericSerializableSettings settings, ValueConverter valueConverter) { + super(name, settings, valueConverter); + } + + @SuppressWarnings("unchecked") + protected ValueCollectionValue getValue() { + return (ValueCollectionValue) settings.getValue(settingName); + } + + protected abstract ValueCollectionValue createValue(); + + private ValueCollectionValue ensureValue() { + ValueCollectionValue result = getValue(); + if(result == null) { + result = createValue(); + settings.setValue(settingName, result); + } + return result; + } + + @Override + public Iterable getValues() { + ValueCollectionValue value = getValue(); + if(value == null) { + return Collections.emptyList(); + } + return value.getValues(getValueConverter()); + } + + @Override + public final void setValues(Iterable values) { + ensureValue().setValues(values, getValueConverter()); + } + + public void addValue(T value) { + ensureValue().addValue(value, getValueConverter()); + } + + public void clear() { + ensureValue().clear(); + } + + @Override + public String toString() { + ValueCollectionValue value = getValue(); + if(value == null) { + return "[]"; + } + return value.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + ValueCollectionValue value = getValue(); + result = prime * result + ((value == null) ? 0 : getValue().hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + AbstractValueCollectionSetting other = (AbstractValueCollectionSetting) obj; + ValueCollectionValue value = getValue(); + ValueCollectionValue otherValue = other.getValue(); + if (value == null) { + if (otherValue != null) + return false; + } else if (!value.equals(otherValue)) + return false; + return true; + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueListSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueListSetting.java new file mode 100644 index 00000000000..966337840d9 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueListSetting.java @@ -0,0 +1,33 @@ +package com.sap.sse.common.settings.generic.base; + +import java.util.List; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.Value; +import com.sap.sse.common.settings.value.ValueCollectionValue; +import com.sap.sse.common.settings.value.ValueListValue; + +public abstract class AbstractValueListSetting extends AbstractValueCollectionSetting> { + public AbstractValueListSetting(String name, AbstractGenericSerializableSettings settings, ValueConverter valueConverter) { + super(name, settings, valueConverter); + } + + @Override + protected ValueCollectionValue> createValue() { + return new ValueListValue(); + } + + @Override + public void resetToDefault() { + clear(); + } + + @Override + public boolean isDefaultValue() { + ValueCollectionValue> value = getValue(); + // explicit default values are possible to implement + // currently, empty is always default + return value == null || value.isEmpty(); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueSetSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueSetSetting.java new file mode 100644 index 00000000000..455ceaf3829 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueSetSetting.java @@ -0,0 +1,76 @@ +package com.sap.sse.common.settings.generic.base; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.sap.sse.common.Util; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.generic.ValueSetSetting; +import com.sap.sse.common.settings.value.Value; +import com.sap.sse.common.settings.value.ValueCollectionValue; +import com.sap.sse.common.settings.value.ValueSetValue; + +public abstract class AbstractValueSetSetting extends AbstractValueCollectionSetting> implements ValueSetSetting { + + private final Set defaultValues = new HashSet<>(); + private final boolean emptyIsDefault; + + public AbstractValueSetSetting(String name, AbstractGenericSerializableSettings settings, ValueConverter valueConverter) { + this(name, settings, false, valueConverter); + } + + public AbstractValueSetSetting(String name, AbstractGenericSerializableSettings settings, boolean emptyIsDefault, ValueConverter valueConverter) { + super(name, settings, valueConverter); + this.emptyIsDefault = emptyIsDefault; + } + + public AbstractValueSetSetting(String name, AbstractGenericSerializableSettings settings, Iterable defaultValues, ValueConverter valueConverter) { + this(name, settings, defaultValues, false, valueConverter); + } + + public AbstractValueSetSetting(String name, AbstractGenericSerializableSettings settings, Iterable defaultValues, boolean emptyIsDefault, ValueConverter valueConverter) { + this(name, settings, emptyIsDefault, valueConverter); + setDefaultValues(defaultValues); + resetToDefault(); + } + + @Override + protected ValueCollectionValue> createValue() { + return new ValueSetValue(); + } + + @Override + public boolean isDefaultValue() { + ValueCollectionValue> value = getValue(); + return (emptyIsDefault && (value == null || value.isEmpty())) + || (value.size() == defaultValues.size() && defaultValues.containsAll(value.getValues(getValueConverter()))); + } + + @Override + public Iterable getValues() { + ValueCollectionValue> value = getValue(); + if(emptyIsDefault && (value == null || value.isEmpty())) { + return Collections.unmodifiableCollection(defaultValues); + } + return super.getValues(); + } + + @Override + public void resetToDefault() { + setValues(defaultValues); + } + + @Override + public final void setDefaultValues(Iterable defaultValues) { + boolean wasDefault = isDefaultValue(); + this.defaultValues.clear(); + if(defaultValues != null) { + Util.addAll(defaultValues, this.defaultValues); + } + if(wasDefault) { + resetToDefault(); + } + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueSetting.java new file mode 100644 index 00000000000..2c66e88e741 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/base/AbstractValueSetting.java @@ -0,0 +1,85 @@ +package com.sap.sse.common.settings.generic.base; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.generic.ValueSetting; +import com.sap.sse.common.settings.value.Value; + +public abstract class AbstractValueSetting extends AbstractHasValueSetting implements ValueSetting { + + private T defaultValue; + + protected AbstractValueSetting(String name, AbstractGenericSerializableSettings settings, T defaultValue, ValueConverter valueConverter) { + super(name, settings, valueConverter); + this.defaultValue = defaultValue; + if(settings.getValue(settingName) == null) { + resetToDefault(); + } + } + + @Override + public T getValue() { + Value value = settings.getValue(settingName); + if(value == null) { + return null; + } + return getValueConverter().fromValue(value); + } + + @Override + public void setValue(T value) { + settings.setValue(settingName, getValueConverter().toValue(value)); + } + + @Override + public boolean isDefaultValue() { + T value = getValue(); + if(value == defaultValue) { + return true; + } + if(value != null) { + return value.equals(defaultValue); + } + // value == null && defaultValue != null => value isn't default + return false; + } + + @Override + public void resetToDefault() { + setValue(defaultValue); + } + + @Override + public String toString() { + return "" + getValue(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + T value = getValue(); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + AbstractValueSetting other = (AbstractValueSetting) obj; + T value = getValue(); + Object otherValue = other.getValue(); + if (value == null) { + if (otherValue != null) + return false; + } else if (!value.equals(otherValue)) + return false; + return true; + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/BooleanConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/BooleanConverter.java new file mode 100644 index 00000000000..e7ac0e80900 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/BooleanConverter.java @@ -0,0 +1,43 @@ +package com.sap.sse.common.settings.generic.converter; + +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.BooleanValue; +import com.sap.sse.common.settings.value.Value; + +public class BooleanConverter implements ValueConverter { + + public static final BooleanConverter INSTANCE = new BooleanConverter(); + + private BooleanConverter() { + } + + @Override + public Object toJSONValue(Boolean value) { + return value; + } + + @Override + public Boolean fromJSONValue(Object jsonValue) { + return (Boolean) jsonValue; + } + + @Override + public String toStringValue(Boolean value) { + return value == null ? null : value.toString(); + } + + @Override + public Boolean fromStringValue(String stringValue) { + return stringValue == null ? null : Boolean.valueOf(stringValue); + } + + @Override + public Boolean fromValue(Value value) { + return ((BooleanValue) value).getValue(); + } + + @Override + public Value toValue(Boolean value) { + return new BooleanValue(value); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/DecimalConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/DecimalConverter.java new file mode 100644 index 00000000000..153005314c0 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/DecimalConverter.java @@ -0,0 +1,45 @@ +package com.sap.sse.common.settings.generic.converter; + +import java.math.BigDecimal; + +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.DecimalValue; +import com.sap.sse.common.settings.value.Value; + +public class DecimalConverter implements ValueConverter { + + public static final DecimalConverter INSTANCE = new DecimalConverter(); + + private DecimalConverter() { + } + + @Override + public Object toJSONValue(BigDecimal value) { + return toStringValue(value); + } + + @Override + public BigDecimal fromJSONValue(Object jsonValue) { + return fromStringValue((String) jsonValue); + } + + @Override + public String toStringValue(BigDecimal value) { + return value == null ? null : value.toString(); + } + + @Override + public BigDecimal fromStringValue(String stringValue) { + return stringValue == null ? null : new BigDecimal(stringValue); + } + + @Override + public BigDecimal fromValue(Value value) { + return ((DecimalValue) value).getValue(); + } + + @Override + public Value toValue(BigDecimal value) { + return new DecimalValue(value); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/DoubleConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/DoubleConverter.java new file mode 100644 index 00000000000..4eca20624a8 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/DoubleConverter.java @@ -0,0 +1,43 @@ +package com.sap.sse.common.settings.generic.converter; + +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.DoubleValue; +import com.sap.sse.common.settings.value.Value; + +public class DoubleConverter implements ValueConverter { + + public static final DoubleConverter INSTANCE = new DoubleConverter(); + + private DoubleConverter() { + } + + @Override + public Object toJSONValue(Double value) { + return value; + } + + @Override + public Double fromJSONValue(Object jsonValue) { + return (Double) jsonValue; + } + + @Override + public String toStringValue(Double value) { + return value == null ? null : value.toString(); + } + + @Override + public Double fromStringValue(String stringValue) { + return stringValue == null ? null : Double.valueOf(stringValue); + } + + @Override + public Double fromValue(Value value) { + return ((DoubleValue) value).getValue(); + } + + @Override + public Value toValue(Double value) { + return new DoubleValue(value); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/EnumConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/EnumConverter.java new file mode 100644 index 00000000000..9d66738c2e3 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/EnumConverter.java @@ -0,0 +1,45 @@ +package com.sap.sse.common.settings.generic.converter; + +import com.sap.sse.common.settings.generic.StringToEnumConverter; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.StringValue; +import com.sap.sse.common.settings.value.Value; + +public class EnumConverter> implements ValueConverter { + + private StringToEnumConverter stringToEnumConverter; + + public EnumConverter(StringToEnumConverter stringToEnumConverter) { + this.stringToEnumConverter = stringToEnumConverter; + } + + @Override + public Object toJSONValue(T value) { + return value == null ? null : value.name(); + } + + @Override + public T fromJSONValue(Object jsonValue) { + return fromStringValue((String) jsonValue); + } + + @Override + public String toStringValue(T value) { + return value == null ? null : value.name(); + } + + @Override + public T fromStringValue(String stringValue) { + return stringValue == null ? null : stringToEnumConverter.fromString(stringValue); + } + + @Override + public T fromValue(Value value) { + return fromStringValue(((StringValue) value).getValue()); + } + + @Override + public Value toValue(T value) { + return new StringValue(toStringValue(value)); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/StringConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/StringConverter.java new file mode 100644 index 00000000000..fc72bea4dbc --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/StringConverter.java @@ -0,0 +1,43 @@ +package com.sap.sse.common.settings.generic.converter; + +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.StringValue; +import com.sap.sse.common.settings.value.Value; + +public class StringConverter implements ValueConverter { + + public static final StringConverter INSTANCE = new StringConverter(); + + private StringConverter() { + } + + @Override + public Object toJSONValue(String value) { + return value; + } + + @Override + public String fromJSONValue(Object jsonValue) { + return (String) jsonValue; + } + + @Override + public String toStringValue(String value) { + return value; + } + + @Override + public String fromStringValue(String stringValue) { + return stringValue; + } + + @Override + public String fromValue(Value value) { + return ((StringValue) value).getValue(); + } + + @Override + public Value toValue(String value) { + return new StringValue(value); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/UUIDConverter.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/UUIDConverter.java new file mode 100644 index 00000000000..e1e18c3b63c --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/generic/converter/UUIDConverter.java @@ -0,0 +1,45 @@ +package com.sap.sse.common.settings.generic.converter; + +import java.util.UUID; + +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.value.UUIDValue; +import com.sap.sse.common.settings.value.Value; + +public class UUIDConverter implements ValueConverter { + + public static final UUIDConverter INSTANCE = new UUIDConverter(); + + private UUIDConverter() { + } + + @Override + public Object toJSONValue(UUID value) { + return toStringValue(value); + } + + @Override + public UUID fromJSONValue(Object jsonValue) { + return fromStringValue((String) jsonValue); + } + + @Override + public String toStringValue(UUID value) { + return value == null ? null : value.toString(); + } + + @Override + public UUID fromStringValue(String stringValue) { + return stringValue == null ? null : UUID.fromString(stringValue); + } + + @Override + public UUID fromValue(Value value) { + return ((UUIDValue) value).getValue(); + } + + @Override + public Value toValue(UUID value) { + return new UUIDValue(value); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/serializer/AbstractSettingsToJsonSerializer.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/serializer/AbstractSettingsToJsonSerializer.java new file mode 100644 index 00000000000..516ba58d90f --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/serializer/AbstractSettingsToJsonSerializer.java @@ -0,0 +1,145 @@ +package com.sap.sse.common.settings.serializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.CollectionSetting; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.Setting; +import com.sap.sse.common.settings.generic.SettingsListSetting; +import com.sap.sse.common.settings.generic.ValueCollectionSetting; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.generic.ValueSetting; + +/** + * Base class for {@link Settings} serializers that produce JSON objects using some generic object model. This base + * class ensures that the produced structure is equivalent for different JSON APIs so the setrialized JSON Strings + * should be compatible and sharable between all those implementations. + * + * @param + * JSON object type + * @param + * JSON Array type + */ +public abstract class AbstractSettingsToJsonSerializer { + + protected abstract OBJECT newOBJECT(); + + protected abstract void set(OBJECT jsonObject, String property, Object value); + + protected abstract Object get(OBJECT jsonObject, String property); + + protected abstract boolean hasProperty(OBJECT jsonObject, String property); + + protected abstract ARRAY ToJsonArray(Iterable values); + + protected abstract Iterable fromJsonArray(ARRAY jsonArray); + + public OBJECT serialize(GenericSerializableSettings settings) { + final OBJECT jsonObject = newOBJECT(); + for (Map.Entry entry : settings.getChildSettings().entrySet()) { + Setting setting = entry.getValue(); + if (!setting.isDefaultValue()) { + set(jsonObject, entry.getKey(), serialize(setting)); + } + } + return jsonObject; + } + + private Object serialize(Setting setting) { + if (setting instanceof ValueSetting) { + return serializeValueSetting((ValueSetting) setting); + } else if (setting instanceof CollectionSetting) { + return serializeListSetting((CollectionSetting) setting); + } else if (setting instanceof GenericSerializableSettings) { + return serialize((GenericSerializableSettings) setting); + } else { + throw new IllegalStateException("Unknown Setting type"); + } + } + + private Object serializeValueSetting(ValueSetting valueSetting) { + return valueSetting.getValueConverter().toJSONValue(valueSetting.getValue()); + } + + private Object serializeListSetting(CollectionSetting listSetting) { + List jsonValues = new ArrayList<>(); + if (listSetting instanceof ValueCollectionSetting) { + ValueCollectionSetting valueListSetting = (ValueCollectionSetting) listSetting; + ValueConverter converter = valueListSetting.getValueConverter(); + for (T value : valueListSetting.getValues()) { + jsonValues.add(converter.toJSONValue(value)); + } + } else if (listSetting instanceof SettingsListSetting) { + SettingsListSetting settingsListSetting = (SettingsListSetting) listSetting; + for (GenericSerializableSettings value : settingsListSetting.getValues()) { + jsonValues.add(serialize(value)); + } + } else { + throw new IllegalStateException("Unknown ListSetting type"); + } + return ToJsonArray(jsonValues); + } + + public T deserialize(T settings, OBJECT json) { + if (json != null) { + for (Map.Entry entry : settings.getChildSettings().entrySet()) { + Setting setting = entry.getValue(); + if(hasProperty(json, entry.getKey())) { + Object jsonValue = get(json, entry.getKey()); + deserializeSetting(setting, jsonValue); + } + } + } + return settings; + } + + private void deserializeSetting(Setting setting, Object jsonValue) { + if (setting instanceof ValueSetting) { + deserializeValueSetting(jsonValue, (ValueSetting) setting); + } else if (setting instanceof CollectionSetting) { + deserializeListSetting(jsonValue, (CollectionSetting) setting); + } else if (setting instanceof GenericSerializableSettings) { + deserializeObject((GenericSerializableSettings) setting, jsonValue); + } else { + throw new IllegalStateException("Unknown Setting type"); + } + } + + @SuppressWarnings("unchecked") + private T deserializeObject(T setting, Object jsonValue) { + return deserialize(setting, (OBJECT) jsonValue); + } + + private void deserializeValueSetting(Object jsonValue, ValueSetting valueSetting) { + valueSetting.setValue(valueSetting.getValueConverter().fromJSONValue(jsonValue)); + } + + private void deserializeListSetting(Object jsonValue, CollectionSetting listSetting) { + @SuppressWarnings("unchecked") + ARRAY jsonArray = (ARRAY) jsonValue; + if (listSetting instanceof ValueCollectionSetting) { + ValueCollectionSetting valueListSetting = (ValueCollectionSetting) listSetting; + ValueConverter converter = valueListSetting.getValueConverter(); + List values = new ArrayList<>(); + for (Object value : fromJsonArray(jsonArray)) { + values.add(converter.fromJSONValue(value)); + } + valueListSetting.setValues(values); + } else if (listSetting instanceof SettingsListSetting) { + deserializeSettingsListSetting(jsonArray, (SettingsListSetting) listSetting); + } else { + throw new IllegalStateException("Unknown ListSetting type"); + } + } + + private void deserializeSettingsListSetting(ARRAY jsonArray, SettingsListSetting settingsListSetting) { + List values = new ArrayList<>(); + for (Object value : fromJsonArray(jsonArray)) { + values.add(deserializeObject(settingsListSetting.getSettingsFactory().newInstance(), value)); + } + settingsListSetting.setValues(values); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/serializer/SettingsToStringMapSerializer.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/serializer/SettingsToStringMapSerializer.java new file mode 100644 index 00000000000..e95032e1853 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/serializer/SettingsToStringMapSerializer.java @@ -0,0 +1,201 @@ +package com.sap.sse.common.settings.serializer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.HasValueSetting; +import com.sap.sse.common.settings.generic.Setting; +import com.sap.sse.common.settings.generic.SettingsListSetting; +import com.sap.sse.common.settings.generic.ValueCollectionSetting; +import com.sap.sse.common.settings.generic.ValueConverter; +import com.sap.sse.common.settings.generic.ValueSetting; + +/** + * {@link GenericSerializableSettings} serializer that constructs flattened String keys with associated String values from the hierarchical + * {@link GenericSerializableSettings} structures. This is necessary for cases where hierarchical structures aren't mappable (e.g. URL + * serialization). + * + */ +public class SettingsToStringMapSerializer { + + public Map> serialize(GenericSerializableSettings settings) { + final Map> result = new HashMap<>(); + serialize("", settings, result); + return result; + } + + private void serialize(String prefix, GenericSerializableSettings settings, Map> serialized) { + for (Map.Entry entry : settings.getChildSettings().entrySet()) { + final Setting setting = entry.getValue(); + if (!setting.isDefaultValue()) { + String key = prefix + entry.getKey(); + serialize(key, setting, serialized); + } + } + } + + private void serialize(String key, Setting setting, Map> serialized) { + if (setting instanceof ValueSetting) { + serialized.put(key, serializeValueSetting(key, (ValueSetting) setting)); + } else if (setting instanceof ValueCollectionSetting) { + serialized.put(key, serializeValueListSetting(key, (ValueCollectionSetting) setting)); + } else { + String prefix = key + GenericSerializableSettings.PATH_SEPARATOR; + if (setting instanceof GenericSerializableSettings) { + serialize(prefix, (GenericSerializableSettings) setting, serialized); + } else if (setting instanceof SettingsListSetting) { + serializeSettingsListSetting(prefix, (SettingsListSetting) setting, serialized); + } else { + throw new IllegalStateException("Unknown Setting type"); + } + } + } + + private void serializeSettingsListSetting(String prefix, SettingsListSetting setting, + Map> serialized) { + int index = 0; + for (T childSettings : setting.getValues()) { + String nestedPrefix = prefix + index + GenericSerializableSettings.PATH_SEPARATOR; + serialize(nestedPrefix, childSettings, serialized); + index++; + } + } + + private Iterable serializeValueSetting(String key, ValueSetting valueSetting) { + return Collections.singleton(valueSetting.getValueConverter().toStringValue(valueSetting.getValue())); + } + + private Iterable serializeValueListSetting(String key, ValueCollectionSetting valueSetting) { + List result = new ArrayList<>(); + ValueConverter valueConverter = valueSetting.getValueConverter(); + for (T value : valueSetting.getValues()) { + result.add(valueConverter.toStringValue(value)); + } + return result; + } + + public T deserialize(T settings, Map> values) { + Map>> mappedInnerValues = mapNested(values); + for (Map.Entry entry : settings.getChildSettings().entrySet()) { + final String key = entry.getKey(); + final Setting setting = entry.getValue(); + deserialize(key, setting, values, mappedInnerValues); + } + return settings; + } + + private void deserialize(String key, Setting setting, Map> values, + Map>> mappedInnerValues) { + if (setting instanceof HasValueSetting) { + final Iterable settingValues = values.get(key); + if (settingValues != null) { + if (setting instanceof ValueSetting) { + deserializeValueSetting((ValueSetting) setting, settingValues); + } else if (setting instanceof ValueCollectionSetting) { + deserializeValueListSetting((ValueCollectionSetting) setting, settingValues); + } else { + throw new IllegalStateException("Unknown HasValueSetting type"); + } + } + } else { + final Map> innerValues = mappedInnerValues.get(key); + if (innerValues != null) { + if (setting instanceof GenericSerializableSettings) { + deserialize((GenericSerializableSettings) setting, innerValues); + } else if (setting instanceof SettingsListSetting) { + deserializeSettingsListSetting((SettingsListSetting) setting, innerValues); + } else { + throw new IllegalStateException("Unknown Setting type"); + } + } + } + } + + private void deserializeValueSetting(ValueSetting valueSetting, Iterable values) { + Iterator iterator = values.iterator(); + if (iterator.hasNext()) { + // Additional values are currently silently skipped. + // This should only happen if somebody manipulates the data or a List setting is changed to a single setting + // value. + valueSetting.setValue(valueSetting.getValueConverter().fromStringValue(iterator.next())); + } + } + + private void deserializeValueListSetting(ValueCollectionSetting valueSetting, Iterable values) { + List deserializedValues = new ArrayList<>(); + for (String stringValue : values) { + deserializedValues.add(valueSetting.getValueConverter().fromStringValue(stringValue)); + } + valueSetting.setValues(deserializedValues); + } + + private void deserializeSettingsListSetting(SettingsListSetting valueSetting, + Map> values) { + Map>> mappedNestedListValues = mapNestedWithIndexOrder(values); + List deserializedValues = new ArrayList<>(); + for (Map.Entry>> entry : mappedNestedListValues.entrySet()) { + deserializedValues.add(deserialize(valueSetting.getSettingsFactory().newInstance(), entry.getValue())); + } + valueSetting.setValues(deserializedValues); + } + + private Map>> mapNestedWithIndexOrder(Map> values) { + Map>> result = new TreeMap<>(new IndexComparator()); + result.putAll(mapNested(values)); + return result; + } + + private Map>> mapNested(Map> values) { + Map>> result = new HashMap<>(); + for (Map.Entry> entry : values.entrySet()) { + String key = entry.getKey(); + int separatorIndex = key.indexOf(GenericSerializableSettings.PATH_SEPARATOR); + if (separatorIndex > 0) { + String group = key.substring(0, separatorIndex); + String nestedKey = key.substring(separatorIndex + 1); + + Map> inner = result.get(group); + if (inner == null) { + inner = new HashMap<>(); + result.put(group, inner); + } + inner.put(nestedKey, entry.getValue()); + } + } + return result; + } + + private static class IndexComparator implements Comparator { + @Override + public int compare(String o1, String o2) { + boolean o1error = false; + boolean o2error = false; + int o1int = 0; + int o2int = 0; + try { + o1int = Integer.parseInt(o1); + } catch (NumberFormatException e) { + o1error = true; + } + try { + o2int = Integer.parseInt(o2); + } catch (NumberFormatException e) { + o1error = true; + } + if (o1error && o2error) { + return o1.compareTo(o2); + } + if (!o1error && !o2error) { + return Integer.compare(o1int, o2int); + } + return o1error ? 1 : -1; + } + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/EnumSetting.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/AbstractValue.java old mode 100755 new mode 100644 similarity index 62% rename from java/com.sap.sse.common/src/com/sap/sse/common/settings/EnumSetting.java rename to java/com.sap.sse.common/src/com/sap/sse/common/settings/value/AbstractValue.java index 2b1a593dfe0..e2e55874ffd --- a/java/com.sap.sse.common/src/com/sap/sse/common/settings/EnumSetting.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/AbstractValue.java @@ -1,49 +1,47 @@ -package com.sap.sse.common.settings; - -public class EnumSetting> implements Setting { - private final Enum value; - - public EnumSetting(Enum value) { - super(); - this.value = value; - } - - public Enum getValue() { - return value; - } - - @Override - public SettingType getType() { - return SettingType.ENUM; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((value == null) ? 0 : value.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - EnumSetting other = (EnumSetting) obj; - if (value == null) { - if (other.value != null) - return false; - } else if (!value.equals(other.value)) - return false; - return true; - } - - @Override - public String toString() { - return value.name(); - } -} +package com.sap.sse.common.settings.value; + +public abstract class AbstractValue implements Value { + private static final long serialVersionUID = -6645573771084361853L; + private T value; + + protected AbstractValue() { + } + + public AbstractValue(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractValue other = (AbstractValue) obj; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + return "" + value; + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/BooleanValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/BooleanValue.java new file mode 100644 index 00000000000..244005e57c6 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/BooleanValue.java @@ -0,0 +1,13 @@ +package com.sap.sse.common.settings.value; + +public class BooleanValue extends AbstractValue { + private static final long serialVersionUID = -4737742445276300510L; + + protected BooleanValue() { + } + + public BooleanValue(Boolean value) { + super(value); + } + +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/DecimalValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/DecimalValue.java new file mode 100644 index 00000000000..5d2b8a63f86 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/DecimalValue.java @@ -0,0 +1,15 @@ +package com.sap.sse.common.settings.value; + +import java.math.BigDecimal; + +public class DecimalValue extends AbstractValue { + private static final long serialVersionUID = -2607692358788082588L; + + protected DecimalValue() { + } + + public DecimalValue(BigDecimal value) { + super(value); + } + +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/DoubleValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/DoubleValue.java new file mode 100644 index 00000000000..431fd4a1932 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/DoubleValue.java @@ -0,0 +1,13 @@ +package com.sap.sse.common.settings.value; + +public class DoubleValue extends AbstractValue { + private static final long serialVersionUID = -615238608843879977L; + + protected DoubleValue() { + } + + public DoubleValue(Double value) { + super(value); + } + +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/SettingsListValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/SettingsListValue.java new file mode 100644 index 00000000000..decbc97a900 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/SettingsListValue.java @@ -0,0 +1,23 @@ +package com.sap.sse.common.settings.value; + +import java.util.HashMap; + +public class SettingsListValue { + + private HashMap values = new HashMap<>(); + + public SettingsListValue() { + } + + public Value getValue(String name) { + return values.get(name); + } + + public void setValue(String name, Value value) { + if(value == null) { + values.remove(name); + } else { + values.put(name, value); + } + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/SettingsValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/SettingsValue.java new file mode 100644 index 00000000000..3b5f984465b --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/SettingsValue.java @@ -0,0 +1,53 @@ +package com.sap.sse.common.settings.value; + +import java.util.HashMap; + +public class SettingsValue implements Value { + private static final long serialVersionUID = -3407605464729611500L; + private HashMap values = new HashMap<>(); + + public SettingsValue() { + } + + public Value getValue(String name) { + return values.get(name); + } + + public void setValue(String name, Value value) { + if(value == null) { + values.remove(name); + } else { + values.put(name, value); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((values == null) ? 0 : values.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SettingsValue other = (SettingsValue) obj; + if (values == null) { + if (other.values != null) + return false; + } else if (!values.equals(other.values)) + return false; + return true; + } + + @Override + public String toString() { + return values.toString(); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/StringValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/StringValue.java new file mode 100644 index 00000000000..8ffc58d806b --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/StringValue.java @@ -0,0 +1,13 @@ +package com.sap.sse.common.settings.value; + +public class StringValue extends AbstractValue { + private static final long serialVersionUID = 4405912516159515483L; + + protected StringValue() { + } + + public StringValue(String value) { + super(value); + } + +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/UUIDValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/UUIDValue.java new file mode 100644 index 00000000000..6431ca75c5f --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/UUIDValue.java @@ -0,0 +1,15 @@ +package com.sap.sse.common.settings.value; + +import java.util.UUID; + +public class UUIDValue extends AbstractValue { + private static final long serialVersionUID = 4218811543529632907L; + + protected UUIDValue() { + } + + public UUIDValue(UUID value) { + super(value); + } + +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/Value.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/Value.java new file mode 100644 index 00000000000..edeaaff1d0b --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/Value.java @@ -0,0 +1,6 @@ +package com.sap.sse.common.settings.value; + +import java.io.Serializable; + +public interface Value extends Serializable { +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java new file mode 100644 index 00000000000..1bc8da872b5 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollection.java @@ -0,0 +1,13 @@ +package com.sap.sse.common.settings.value; + +import com.sap.sse.common.settings.generic.ValueConverter; + +public interface ValueCollection extends Value { + Iterable getValues(ValueConverter converter); + + void setValues(Iterable values, ValueConverter converter); + + void addValue(T value, ValueConverter converter); + + void clear(); +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java new file mode 100644 index 00000000000..80c49aad086 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueCollectionValue.java @@ -0,0 +1,92 @@ +package com.sap.sse.common.settings.value; + +import java.util.Collection; +import java.util.Collections; + +import com.sap.sse.common.settings.generic.ValueConverter; + +public abstract class ValueCollectionValue> implements ValueCollection { + private static final long serialVersionUID = -5820765644801217519L; + private C values; + + protected ValueCollectionValue(C values) { + this.values = values; + } + + protected abstract Collection emptyCollection(); + + @Override + public Collection getValues(ValueConverter converter) { + Collection result = emptyCollection(); + for (Value value : values) { + result.add(converter.fromValue(value)); + } + return result; + } + + public Iterable getValueObjects() { + return Collections.unmodifiableCollection(values); + } + + @Override + public void setValues(Iterable values, ValueConverter converter) { + clear(); + if(values != null) { + for (T value : values) { + addValue(value, converter); + } + } + } + + @Override + public void clear() { + this.values.clear(); + } + + @Override + public void addValue(T value, ValueConverter converter) { + this.values.add(converter.toValue(value)); + } + + public void addValue(Value value) { + this.values.add(value); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public int size() { + return values.size(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((values == null) ? 0 : values.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ValueCollectionValue other = (ValueCollectionValue) obj; + if (values == null) { + if (other.values != null) + return false; + } else if (!values.equals(other.values)) + return false; + return true; + } + + @Override + public String toString() { + return values.toString(); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueListValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueListValue.java new file mode 100644 index 00000000000..1172a1d75a6 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueListValue.java @@ -0,0 +1,18 @@ +package com.sap.sse.common.settings.value; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ValueListValue extends ValueCollectionValue> { + private static final long serialVersionUID = -5820765644801217519L; + + public ValueListValue() { + super(new ArrayList()); + } + + @Override + protected Collection emptyCollection() { + return new ArrayList<>(); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueSetValue.java b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueSetValue.java new file mode 100644 index 00000000000..bb1fd4fd7ed --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/settings/value/ValueSetValue.java @@ -0,0 +1,18 @@ +package com.sap.sse.common.settings.value; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class ValueSetValue extends ValueCollectionValue> { + private static final long serialVersionUID = -5820765644801217519L; + + public ValueSetValue() { + super(new HashSet()); + } + + @Override + protected Collection emptyCollection() { + return new HashSet<>(); + } +} diff --git a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF index 0964eceecfb..fbe847897ab 100644 --- a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF @@ -58,5 +58,6 @@ Export-Package: com.google.gwt.user.client.rpc.core.java.util, com.sap.sse.gwt.resources, com.sap.sse.gwt.server, com.sap.sse.gwt.server.filestorage, + com.sap.sse.gwt.settings, com.sap.sse.gwt.shared, com.sap.sse.gwt.shared.filestorage diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/Settings.gwt.xml b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/Settings.gwt.xml new file mode 100644 index 00000000000..8d65cd9293e --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/Settings.gwt.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/GWTLocaleUtil.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/GWTLocaleUtil.java similarity index 94% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/GWTLocaleUtil.java rename to java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/GWTLocaleUtil.java index d9265b0c6b1..61e7fc41274 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/GWTLocaleUtil.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/GWTLocaleUtil.java @@ -1,7 +1,8 @@ -package com.sap.sailing.gwt.common.client; +package com.sap.sse.gwt.client; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import com.google.gwt.i18n.client.LocaleInfo; @@ -35,7 +36,7 @@ public final class GWTLocaleUtil { * * @return all available client locales plus a null entry */ - public static Iterable getAvailableLocalesAndDefault() { + public static Collection getAvailableLocalesAndDefault() { List result = new ArrayList<>(); result.add(null); Util.addAll(getAvailableLocales(), result); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/VoidResult.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/VoidResult.java new file mode 100644 index 00000000000..694becc1c94 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/VoidResult.java @@ -0,0 +1,7 @@ +package com.sap.sse.gwt.dispatch.shared.commands; + +/** + * Empty {@link Result} implementation, which can be reused for {@link Action}s with a void result. + */ +public class VoidResult implements Result { +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/AbstractEntryPointWithSettingsLinkFactory.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/AbstractEntryPointWithSettingsLinkFactory.java new file mode 100644 index 00000000000..878460e4021 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/AbstractEntryPointWithSettingsLinkFactory.java @@ -0,0 +1,17 @@ +package com.sap.sse.gwt.settings; + +import com.google.gwt.http.client.UrlBuilder; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; + +public abstract class AbstractEntryPointWithSettingsLinkFactory { + + private static final SettingsToUrlSerializer serializer = new SettingsToUrlSerializer(); + + protected static String createEntryPointLink(String baseLink, GenericSerializableSettings... settings) { + UrlBuilder urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParameters(baseLink); + for (GenericSerializableSettings setting : settings) { + serializer.serializeToUrlBuilder(setting, urlBuilder); + } + return urlBuilder.buildString(); + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/SettingsToJsonSerializerGWT.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/SettingsToJsonSerializerGWT.java new file mode 100755 index 00000000000..eaa90ec1114 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/SettingsToJsonSerializerGWT.java @@ -0,0 +1,119 @@ +package com.sap.sse.gwt.settings; + +import java.util.ArrayList; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONBoolean; +import com.google.gwt.json.client.JSONNull; +import com.google.gwt.json.client.JSONNumber; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONParser; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.serializer.AbstractSettingsToJsonSerializer; + +/** + * This is a GWT implementation of an {@link AbstractSettingsToJsonSerializer} that uses the json api library available + * on the client side. + * + */ +public class SettingsToJsonSerializerGWT extends AbstractSettingsToJsonSerializer { + + public String serializeToString(GenericSerializableSettings settings) { + return serialize(settings).toString(); + } + + public T deserialize(T settings, String jsonString) { + if (jsonString != null && !jsonString.isEmpty()) { + JSONValue jsonValue = JSONParser.parseStrict(jsonString); + if (jsonValue != null) { + JSONObject jsonObject = jsonValue.isObject(); + if (jsonObject != null) { + deserialize(settings, (JSONObject) jsonObject); + } + } + } + return settings; + } + + @Override + protected JSONObject newOBJECT() { + return new JSONObject(); + } + + @Override + protected void set(JSONObject jsonObject, String property, Object value) { + jsonObject.put(property, toJsonValue(value)); + } + + private JSONValue toJsonValue(Object value) { + if(value == null) { + return JSONNull.getInstance(); + } else if (value instanceof JSONArray) { + return (JSONArray) value; + } else if (value instanceof JSONObject) { + return (JSONObject) value; + } else if (value instanceof String) { + return new JSONString((String) value); + } else if (value instanceof Number) { + return new JSONNumber(((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + return JSONBoolean.getInstance((Boolean) value); + } + throw new IllegalStateException("Non JSON-compatible value found!"); + } + + private Object fromJsonValue(JSONValue jsonValue) { + if(jsonValue.isNull() != null) { + return null; + } + if(jsonValue.isBoolean() != null) { + return jsonValue.isBoolean().booleanValue(); + } + if(jsonValue.isString() != null) { + return jsonValue.isString().stringValue(); + } + if(jsonValue.isNumber() != null) { + return jsonValue.isNumber().doubleValue(); + } + if(jsonValue.isObject() != null) { + return jsonValue.isObject(); + } + if(jsonValue.isArray() != null) { + return jsonValue.isArray(); + } + throw new IllegalStateException("Unexpected JSONValue found!"); + } + + @Override + protected Object get(JSONObject jsonObject, String property) { + return fromJsonValue(jsonObject.get(property)); + } + + @Override + protected boolean hasProperty(JSONObject jsonObject, String property) { + return jsonObject.containsKey(property); + } + + @Override + protected JSONArray ToJsonArray(Iterable values) { + JSONArray jsonArray = new JSONArray(); + for (Object value : values) { + JSONValue jsonValue = toJsonValue(value); + if (jsonValue != null) { + jsonArray.set(jsonArray.size(), jsonValue); + } + } + return jsonArray; + } + + @Override + protected Iterable fromJsonArray(JSONArray jsonArray) { + final ArrayList result = new ArrayList(jsonArray.size()); + for (int i = 0; i < jsonArray.size(); i++) { + result.add(fromJsonValue(jsonArray.get(i))); + } + return result; + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/SettingsToUrlSerializer.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/SettingsToUrlSerializer.java new file mode 100644 index 00000000000..dd2edb81cf5 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/SettingsToUrlSerializer.java @@ -0,0 +1,66 @@ +package com.sap.sse.gwt.settings; + +import java.util.Map; + +import com.google.gwt.http.client.UrlBuilder; +import com.google.gwt.user.client.Window; +import com.sap.sse.common.Util; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.serializer.SettingsToStringMapSerializer; + +/** + * Serializes {@link GenericSerializableSettings} objects to the URL by constructing flat keys from the hierarchical object graph. + * + * This type of serialization has some limitations: + *
      + *
    • Empty collections can not be serialized. This can only occur for collection settings that have their default value set to be non-empty and there is no actual value.
    • + *
    • Null values can not be distinguished from empty strings
    • + *
    + * + */ +public class SettingsToUrlSerializer { + + private final SettingsToStringMapSerializer settingsToStringMapSerializer = new SettingsToStringMapSerializer(); + + public String serializeBasedOnCurrentLocation(GenericSerializableSettings settings) { + final UrlBuilder urlBuilder = Window.Location.createUrlBuilder(); + serializeToUrlBuilder(settings, urlBuilder); + return urlBuilder.buildString(); + } + + public String serializeBasedOnCurrentLocationWithCleanParameters(GenericSerializableSettings settings) { + final UrlBuilder urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParameters(); + serializeToUrlBuilder(settings, urlBuilder); + return urlBuilder.buildString(); + } + + public UrlBuilder serializeUrlBuilderBasedOnCurrentLocation(GenericSerializableSettings settings) { + final UrlBuilder urlBuilder = Window.Location.createUrlBuilder(); + serializeToUrlBuilder(settings, urlBuilder); + return urlBuilder; + } + + public UrlBuilder serializeUrlBuilderBasedOnCurrentLocationWithCleanParameters(GenericSerializableSettings settings) { + final UrlBuilder urlBuilder = UrlBuilderUtil.createUrlBuilderFromCurrentLocationWithCleanParameters(); + serializeToUrlBuilder(settings, urlBuilder); + return urlBuilder; + } + + public void serializeToUrlBuilder(GenericSerializableSettings settings, UrlBuilder urlBuilder) { + Map> serializedValues = settingsToStringMapSerializer.serialize(settings); + for(Map.Entry> entry : serializedValues.entrySet()) { + Iterable parameterValues = entry.getValue(); + String[] parameterValuesAsArray = Util.toArray(parameterValues, new String[Util.size(parameterValues)]); + if (parameterValuesAsArray.length > 0) { + urlBuilder.setParameter(entry.getKey(), parameterValuesAsArray); + } + } + } + + public T deserializeFromCurrentLocation(T settings) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Map> values = (Map) Window.Location.getParameterMap(); + return settingsToStringMapSerializer.deserialize(settings, values); + } + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/UrlBuilderUtil.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/UrlBuilderUtil.java new file mode 100644 index 00000000000..a1522d76e1f --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/settings/UrlBuilderUtil.java @@ -0,0 +1,25 @@ +package com.sap.sse.gwt.settings; + +import com.google.gwt.http.client.UrlBuilder; +import com.google.gwt.user.client.Window; + +public class UrlBuilderUtil { + + public static UrlBuilder createUrlBuilderFromCurrentLocationWithCleanParameters() { + final UrlBuilder urlBuilder = Window.Location.createUrlBuilder(); + for(String parameterName : Window.Location.getParameterMap().keySet()) { + if(!"gwt.codesvr".equals(parameterName) + && !"locale".equals(parameterName)) { + urlBuilder.removeParameter(parameterName); + } + } + urlBuilder.setHash(null); + return urlBuilder; + } + + public static UrlBuilder createUrlBuilderFromCurrentLocationWithCleanParameters(String path) { + final UrlBuilder urlBuilder = createUrlBuilderFromCurrentLocationWithCleanParameters(); + urlBuilder.setPath(path); + return urlBuilder; + } +} diff --git a/java/com.sap.sse.mail/META-INF/MANIFEST.MF b/java/com.sap.sse.mail/META-INF/MANIFEST.MF index d5aff706f24..ab125af96f5 100755 --- a/java/com.sap.sse.mail/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.mail/META-INF/MANIFEST.MF @@ -7,10 +7,13 @@ Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-Activator: com.sap.sse.mail.impl.Activator Export-Package: com.sap.sse.mail, - com.sap.sse.mail.impl + com.sap.sse.mail.impl, + com.sap.sse.mail.queue, + com.sap.sse.mail.queue.impl Import-Package: javax.mail, javax.mail.internet, - org.osgi.framework + org.osgi.framework, + org.osgi.util.tracker;version="1.5.1" Require-Bundle: com.sap.sse.replication, com.sap.sse.operationaltransformation, com.sap.sse.common, diff --git a/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/MailNotification.java b/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/MailNotification.java new file mode 100644 index 00000000000..4dec0c06cbb --- /dev/null +++ b/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/MailNotification.java @@ -0,0 +1,7 @@ +package com.sap.sse.mail.queue; + +import com.sap.sse.mail.MailService; + +public interface MailNotification { + void sendNotifications(final MailService mailService); +} \ No newline at end of file diff --git a/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/MailQueue.java b/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/MailQueue.java new file mode 100644 index 00000000000..b2266073e5b --- /dev/null +++ b/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/MailQueue.java @@ -0,0 +1,11 @@ +package com.sap.sse.mail.queue; + +import com.sap.sse.common.Stoppable; +import com.sap.sse.mail.MailService; + +/** + * Supports sending mails via {@link MailService} in a deferred way by enqueuing {@link MailNotification} instances. + */ +public interface MailQueue extends Stoppable { + void addNotification(MailNotification notification); +} diff --git a/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/impl/ExecutorMailQueue.java b/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/impl/ExecutorMailQueue.java new file mode 100644 index 00000000000..4fea8c29ba0 --- /dev/null +++ b/java/com.sap.sse.mail/src/com/sap/sse/mail/queue/impl/ExecutorMailQueue.java @@ -0,0 +1,38 @@ +package com.sap.sse.mail.queue.impl; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.osgi.util.tracker.ServiceTracker; + +import com.sap.sse.mail.MailService; +import com.sap.sse.mail.queue.MailNotification; +import com.sap.sse.mail.queue.MailQueue; + +/** + * {@link MailQueue} implementation that uses a single threaded executor to process the {@link MailNotification}s. + */ +public class ExecutorMailQueue implements MailQueue { + private final ServiceTracker mailServiceTracker; + + private final ExecutorService executor = Executors.newSingleThreadExecutor( + (runnable) -> { + Thread thread = new Thread(runnable, ExecutorMailQueue.class.getName() + " executor"); + thread.setDaemon(true); + return thread; + }); + + public ExecutorMailQueue(ServiceTracker mailServiceTracker) { + this.mailServiceTracker = mailServiceTracker; + } + + @Override + public void stop() { + executor.shutdownNow(); + } + + @Override + public void addNotification(MailNotification notification) { + executor.execute(() -> notification.sendNotifications(mailServiceTracker.getService())); + } +} diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/JavaIoSerializablePreferenceConverter.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/JavaIoSerializablePreferenceConverter.java new file mode 100644 index 00000000000..eef902e029c --- /dev/null +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/JavaIoSerializablePreferenceConverter.java @@ -0,0 +1,36 @@ +package com.sap.sse.security.test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Base64; + +import com.sap.sse.security.PreferenceConverter; + +public class JavaIoSerializablePreferenceConverter implements PreferenceConverter { + @Override + public String toPreferenceString(T preference) { + try { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(stream); + oos.writeObject(preference); + oos.flush(); + return Base64.getEncoder().encodeToString(stream.toByteArray()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public T toPreferenceObject(String stringPreference) { + try { + return (T) new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(stringPreference))) + .readObject(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} \ No newline at end of file diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java index 9229f35686b..951aaa8a636 100755 --- a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java @@ -34,7 +34,6 @@ public class LoginTest { db.getCollection(CollectionNames.USERS.name()).drop(); db.getCollection(CollectionNames.SETTINGS.name()).drop(); db.getCollection(CollectionNames.PREFERENCES.name()).drop(); - db.getCollection(CollectionNames.PREFERENCES.name()).drop(); store = new UserStoreImpl(); UsernamePasswordRealm.setTestUserStore(store); Activator.setTestUserStore(store); diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PreferenceObjectBasedNotificationSetTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PreferenceObjectBasedNotificationSetTest.java new file mode 100644 index 00000000000..cd9ca899c9d --- /dev/null +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PreferenceObjectBasedNotificationSetTest.java @@ -0,0 +1,291 @@ +package com.sap.sse.security.test; + +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.function.Consumer; + +import org.junit.Before; +import org.junit.Test; + +import com.mongodb.DB; +import com.mongodb.MongoException; +import com.sap.sse.common.Util; +import com.sap.sse.mongodb.MongoDBConfiguration; +import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.security.PreferenceObjectBasedNotificationSet; +import com.sap.sse.security.User; +import com.sap.sse.security.UserStore; +import com.sap.sse.security.shared.UserManagementException; +import com.sap.sse.security.userstore.mongodb.UserStoreImpl; +import com.sap.sse.security.userstore.mongodb.impl.CollectionNames; + +import junit.framework.Assert; + +public class PreferenceObjectBasedNotificationSetTest { + + private static final String A = "a"; + private static final String B = "b"; + private static final String C = "c"; + + private UserStoreImpl store; + + private static final String user1 = "me"; + private static final String user2 = "somebody_else"; + private static final String mail = "anonymous@sapsailing.com"; + + private static final JavaIoSerializablePreferenceConverter> prefConverter = new JavaIoSerializablePreferenceConverter<>(); + private static final String prefKey = "prefKey"; + private static final String otherPrefKey = "otherPrefKey"; + private static final HashSet values1 = values(A, B); + private static final HashSet values2 = values(B, C); + private static final HashSet allValues = values(A, B, C); + + @Before + public void setUp() throws UnknownHostException, MongoException { + final MongoDBConfiguration dbConfiguration = MongoDBConfiguration.getDefaultTestConfiguration(); + final MongoDBService service = dbConfiguration.getService(); + DB db = service.getDB(); + db.getCollection(CollectionNames.USERS.name()).drop(); + db.getCollection(CollectionNames.SETTINGS.name()).drop(); + db.getCollection(CollectionNames.PREFERENCES.name()).drop(); + store = new UserStoreImpl(); + } + + @Test + public void noPreferenceAvailableTest() { + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + Assert.assertTrue(Util.isEmpty(notificationSet.getUsersnamesToNotifyFor(prefKey))); + } + + @Test + public void preferenceAllreadySetTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values())); + } + + @Test + public void preferenceSetAfterNotificationSetCreationTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.setPreferenceObject(user1, prefKey, values1); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values())); + } + + @Test + public void preferenceSetForTwoUsersTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + store.setPreferenceObject(user2, prefKey, values2); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user1, user2))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values(user2))); + } + + @Test + public void preferenceUpdateTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.setPreferenceObject(user1, prefKey, values2); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values(user1))); + } + + @Test + public void preferenceUpdateDoesntInfluenceOtherUserTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + store.setPreferenceObject(user2, prefKey, allValues); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.setPreferenceObject(user1, prefKey, values2); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values(user2))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user1, user2))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values(user1, user2))); + } + + @Test + public void preferenceUnsetDoesntInfluenceOtherUserTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + store.setPreferenceObject(user2, prefKey, values2); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.unsetPreference(user1, prefKey); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user2))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values(user2))); + } + + @Test + public void preferenceUnsetTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.setPreferenceObject(user1, prefKey, values1); + store.unsetPreference(user1, prefKey); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values())); + } + + @Test + public void testForObjectThatIsntKnownTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor("x"), values())); + } + + @Test + public void otherPrefDoesntInfluenceCalculationTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.registerPreferenceConverter(otherPrefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + store.setPreferenceObject(user1, otherPrefKey, values2); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values(user1))); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values())); + } + + @Test + public void noUserMappingTest() { + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + + UserConsumerMock mock = new UserConsumerMock(); + notificationSet.forUsersWithVerifiedEmailMappedTo(A, mock); + Assert.assertEquals(0, mock.calls.size()); + } + + @Test + public void userMappingTest() throws UserManagementException { + createUserWithVerifiedEmail(user1, mail); + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + + UserConsumerMock mock = new UserConsumerMock(); + + notificationSet.forUsersWithVerifiedEmailMappedTo(A, mock); + Assert.assertEquals(users(store.getUserByName(user1)), mock.calls); + } + + @Test + public void userWithNonVerifiedEmailIsSkippedTest() throws UserManagementException { + store.createUser(user1, mail); + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + + UserConsumerMock mock = new UserConsumerMock(); + + notificationSet.forUsersWithVerifiedEmailMappedTo(A, mock); + Assert.assertTrue(mock.calls.isEmpty()); + } + + @Test + public void userMappingWithTwoUsersTest() throws UserManagementException { + createUserWithVerifiedEmail(user1, mail); + createUserWithVerifiedEmail(user2, mail); + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + store.setPreferenceObject(user2, prefKey, values2); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + + UserConsumerMock mock = new UserConsumerMock(); + notificationSet.forUsersWithVerifiedEmailMappedTo(A, mock); + Assert.assertEquals(users(store.getUserByName(user1)), mock.calls); + + mock = new UserConsumerMock(); + notificationSet.forUsersWithVerifiedEmailMappedTo(B, mock); + Assert.assertEquals(users(store.getUserByName(user1), store.getUserByName(user2)), mock.calls); + } + + @Test + public void userMappingWithOneExistingAndOneUnknownUserTest() throws UserManagementException { + createUserWithVerifiedEmail(user1, mail); + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + store.setPreferenceObject(user2, prefKey, values2); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + + UserConsumerMock mock = new UserConsumerMock(); + notificationSet.forUsersWithVerifiedEmailMappedTo(B, mock); + Assert.assertEquals(users(store.getUserByName(user1)), mock.calls); + } + + /** + * There was a bug that caused the preferences not to be removed when a user was deleted. + */ + @Test + public void deleteUserWithMappingTest() throws UserManagementException { + store.createUser(user1, mail); + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.deleteUser(user1); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values())); + } + + @Test + public void removePreferenceConverterTest() throws UserManagementException { + store.createUser(user1, mail); + store.registerPreferenceConverter(prefKey, prefConverter); + store.setPreferenceObject(user1, prefKey, values1); + PreferenceObjectBasedNotificationSetImpl notificationSet = new PreferenceObjectBasedNotificationSetImpl(prefKey, store); + store.removePreferenceConverter(prefKey); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(A), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(B), values())); + Assert.assertTrue(Util.equals(notificationSet.getUsersnamesToNotifyFor(C), values())); + } + + private static HashSet values(String... values) { + return new HashSet<>(Arrays.asList(values)); + } + + private static HashSet users(User... values) { + return new HashSet<>(Arrays.asList(values)); + } + + private void createUserWithVerifiedEmail(String username, String email) throws UserManagementException { + store.createUser(username, email); + store.updateUser(new User(username, email, null, null, null, true, null, null, Collections.emptySet())); + } + + private static class PreferenceObjectBasedNotificationSetImpl extends PreferenceObjectBasedNotificationSet, String> { + + public PreferenceObjectBasedNotificationSetImpl(String key, UserStore store) { + super(key, store); + } + + @Override + protected Collection calculateObjectsToNotify(HashSet preference) { + return preference; + } + } + + private static class UserConsumerMock implements Consumer { + final HashSet calls = new HashSet<>(); + + @Override + public void accept(User user) { + if(user == null) { + throw new IllegalArgumentException("User is null"); + } + calls.add(user); + } + } +} diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserPreferenceObjectAndConverterTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserPreferenceObjectAndConverterTest.java new file mode 100644 index 00000000000..31d53f9ee5c --- /dev/null +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserPreferenceObjectAndConverterTest.java @@ -0,0 +1,178 @@ +package com.sap.sse.security.test; + +import java.io.Serializable; +import java.net.UnknownHostException; + +import org.junit.Before; +import org.junit.Test; + +import com.mongodb.DB; +import com.mongodb.MongoException; +import com.sap.sse.mongodb.MongoDBConfiguration; +import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.security.shared.UserManagementException; +import com.sap.sse.security.userstore.mongodb.UserStoreImpl; +import com.sap.sse.security.userstore.mongodb.impl.CollectionNames; + +import junit.framework.Assert; + +public class UserPreferenceObjectAndConverterTest { + + private UserStoreImpl store; + + private final String email = "anonymous@sapsailing.com"; + private static final String user1 = "me"; + private static final String user2 = "somebody_else"; + + private static final JavaIoSerializablePreferenceConverter prefConverter = new JavaIoSerializablePreferenceConverter<>(); + private static final String prefKey1 = "prefKey1"; + private static final SimplePreferenceForSerialization pref1 = new SimplePreferenceForSerialization("test12345", false, 432.567); + private static final String serializedPref1 = prefConverter.toPreferenceString(pref1); + private static final String prefKey2 = "prefKey2"; + private static final SimplePreferenceForSerialization pref2 = new SimplePreferenceForSerialization("some sailing value", true, 9.87654321); + private static final String serializedPref2 = prefConverter.toPreferenceString(pref2); + + @Before + public void setUp() throws UnknownHostException, MongoException { + final MongoDBConfiguration dbConfiguration = MongoDBConfiguration.getDefaultTestConfiguration(); + final MongoDBService service = dbConfiguration.getService(); + DB db = service.getDB(); + db.getCollection(CollectionNames.USERS.name()).drop(); + db.getCollection(CollectionNames.SETTINGS.name()).drop(); + db.getCollection(CollectionNames.PREFERENCES.name()).drop(); + db.getCollection(CollectionNames.PREFERENCES.name()).drop(); + store = new UserStoreImpl(); + } + + @Test + public void noConverterSetTest() { + store.setPreference(user1, prefKey1, serializedPref1); + Assert.assertNull(store.getPreferenceObject(user1, prefKey1)); + } + + @Test + public void converterAlreadyRegisteredWhenSettingPreferenceTest() { + store.registerPreferenceConverter(prefKey1, prefConverter); + store.setPreference(user1, prefKey1, serializedPref1); + Assert.assertEquals(pref1, store.getPreferenceObject(user1, prefKey1)); + } + + @Test + public void converterSetWhenPreferenceAlreadyRegisteredTest() { + store.setPreference(user1, prefKey1, serializedPref1); + store.registerPreferenceConverter(prefKey1, prefConverter); + Assert.assertEquals(pref1, store.getPreferenceObject(user1, prefKey1)); + } + + @Test + public void preferencesForTwoUsersTest() { + store.setPreference(user1, prefKey1, serializedPref1); + store.setPreference(user2, prefKey1, serializedPref2); + store.registerPreferenceConverter(prefKey1, prefConverter); + Assert.assertEquals(pref1, store.getPreferenceObject(user1, prefKey1)); + Assert.assertEquals(pref2, store.getPreferenceObject(user2, prefKey1)); + } + + @Test + public void twoPreferencesForOneUserTest() { + store.setPreference(user1, prefKey1, serializedPref1); + store.setPreference(user1, prefKey2, serializedPref2); + store.registerPreferenceConverter(prefKey1, prefConverter); + store.registerPreferenceConverter(prefKey2, prefConverter); + Assert.assertEquals(pref1, store.getPreferenceObject(user1, prefKey1)); + Assert.assertEquals(pref2, store.getPreferenceObject(user1, prefKey2)); + } + + @Test + public void setPreferenceObjectTest() { + store.registerPreferenceConverter(prefKey1, prefConverter); + store.setPreferenceObject(user1, prefKey1, pref1); + Assert.assertEquals(serializedPref1, store.getPreference(user1, prefKey1)); + Assert.assertEquals(pref1, store.getPreferenceObject(user1, prefKey1)); + } + + @Test + public void unsetPreferenceTest() { + store.registerPreferenceConverter(prefKey1, prefConverter); + store.setPreferenceObject(user1, prefKey1, pref1); + store.unsetPreference(user1, prefKey1); + Assert.assertNull(store.getPreferenceObject(user1, prefKey1)); + } + + @Test + public void setPreferenceObjectToNullTest() { + store.registerPreferenceConverter(prefKey1, prefConverter); + store.setPreferenceObject(user1, prefKey1, pref1); + store.setPreferenceObject(user1, prefKey1, null); + Assert.assertNull(store.getPreferenceObject(user1, prefKey1)); + } + + /** + * There was a bug that caused the preferences not to be removed when a user was deleted. + */ + @Test + public void deleteUserWithPreferenceObjectTest() throws UserManagementException { + store.createUser(user1, email); + store.registerPreferenceConverter(prefKey1, prefConverter); + store.setPreferenceObject(user1, prefKey1, pref1); + store.deleteUser(user1); + Assert.assertNull(store.getPreferenceObject(user1, prefKey1)); + } + + @Test + public void removeConverterTest() throws UserManagementException { + store.createUser(user1, email); + store.registerPreferenceConverter(prefKey1, prefConverter); + store.setPreference(user1, prefKey1, serializedPref1); + store.removePreferenceConverter(prefKey1); + Assert.assertNull(store.getPreferenceObject(user1, prefKey1)); + } + + public static class SimplePreferenceForSerialization implements Serializable { + private static final long serialVersionUID = -580569932709860895L; + + private String someString; + private boolean someBoolean; + private double soumeDouble; + + public SimplePreferenceForSerialization(String someString, boolean someBoolean, double soumeDouble) { + super(); + this.someString = someString; + this.someBoolean = someBoolean; + this.soumeDouble = soumeDouble; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (someBoolean ? 1231 : 1237); + result = prime * result + ((someString == null) ? 0 : someString.hashCode()); + long temp; + temp = Double.doubleToLongBits(soumeDouble); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SimplePreferenceForSerialization other = (SimplePreferenceForSerialization) obj; + if (someBoolean != other.someBoolean) + return false; + if (someString == null) { + if (other.someString != null) + return false; + } else if (!someString.equals(other.someString)) + return false; + if (Double.doubleToLongBits(soumeDouble) != Double.doubleToLongBits(other.soumeDouble)) + return false; + return true; + } + } +} diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreTest.java index 2b4c91673e4..0f6b1531b99 100644 --- a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreTest.java +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreTest.java @@ -41,4 +41,13 @@ public class UserStoreTest { assertEquals(prefValue, newUserStore.getPreference(username, prefKey)); assertEquals(username, newUserStore.getUserByAccessToken(accessToken).getName()); } + + /** + * There was a bug that caused the preferences not to be removed when a user was deleted. + */ + @Test + public void testDeleteUserWithPreference() throws UserManagementException { + userStore.unsetPreference(username, prefKey); + assertNull(prefValue, userStore.getPreference(username, prefKey)); + } } diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreWithPersistenceTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreWithPersistenceTest.java new file mode 100644 index 00000000000..c152beae7b3 --- /dev/null +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/UserStoreWithPersistenceTest.java @@ -0,0 +1,123 @@ +package com.sap.sse.security.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import com.mongodb.DB; +import com.mongodb.MongoException; +import com.sap.sse.mongodb.MongoDBConfiguration; +import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.security.User; +import com.sap.sse.security.UserStore; +import com.sap.sse.security.shared.UserManagementException; +import com.sap.sse.security.userstore.mongodb.UserStoreImpl; +import com.sap.sse.security.userstore.mongodb.impl.CollectionNames; + +/** + * Tests that the MongoDB persistence is always in sync with the {@link UserStore}. + */ +public class UserStoreWithPersistenceTest { + private final String username = "abc"; + private final String fullName = "Arno Nym"; + private final String company = "SAP SE"; + private final String email = "anonymous@sapsailing.com"; + private final String prefKey = "pk"; + private final String prefValue = "pv"; + + private UserStoreImpl store; + + @Before + public void setUp() throws UnknownHostException, MongoException { + final MongoDBConfiguration dbConfiguration = MongoDBConfiguration.getDefaultTestConfiguration(); + final MongoDBService service = dbConfiguration.getService(); + DB db = service.getDB(); + db.getCollection(CollectionNames.USERS.name()).drop(); + db.getCollection(CollectionNames.SETTINGS.name()).drop(); + db.getCollection(CollectionNames.PREFERENCES.name()).drop(); + db.getCollection(CollectionNames.PREFERENCES.name()).drop(); + newStore(); + } + + private void newStore() { + store = new UserStoreImpl(); + } + + @Test + public void testCreateUser() throws UserManagementException { + store.createUser(username, email); + assertNotNull(store.getUserByName(username)); + assertNotNull(store.getUserByEmail(email)); + + newStore(); + assertNotNull(store.getUserByName(username)); + assertNotNull(store.getUserByEmail(email)); + } + + @Test + public void testMasterdataIsSaved() throws UserManagementException { + store.createUser(username, email); + store.updateUser(new User(username, email, fullName, company, Locale.GERMAN, false, null, null, Collections.emptySet())); + newStore(); + User savedUser = store.getUserByName(username); + assertEquals(username, savedUser.getName()); + assertEquals(email, savedUser.getEmail()); + assertEquals(company, savedUser.getCompany()); + assertEquals(Locale.GERMAN, savedUser.getLocale()); + } + + /** + * There was a {@link NullPointerException} thrown if calling getUserByEmail for an email address for which there + * wasn't an associated user. + */ + @Test + public void testDeleteUser() throws UserManagementException { + store.createUser(username, email); + store.deleteUser(username); + assertNull(store.getUserByName(username)); + assertNull(store.getUserByEmail(email)); + + newStore(); + assertNull(store.getUserByName(username)); + assertNull(store.getUserByEmail(email)); + } + + @Test + public void testSetPreferences() throws UserManagementException { + store.createUser(username, email); + store.setPreference(username, prefKey, prefValue); + assertEquals(prefValue, store.getPreference(username, prefKey)); + newStore(); + assertEquals(prefValue, store.getPreference(username, prefKey)); + } + + @Test + public void testUnsetPreferences() throws UserManagementException { + store.createUser(username, email); + store.setPreference(username, prefKey, prefValue); + store.unsetPreference(username, prefKey); + assertNull(store.getPreference(username, prefKey)); + newStore(); + assertNull(store.getPreference(username, prefKey)); + } + + /** + * There was a bug that caused the preferences not to be removed when a user was deleted. + */ + @Test + public void testDeleteUserWithPreferences() throws UserManagementException { + store.createUser(username, email); + store.setPreference(username, prefKey, prefValue); + store.deleteUser(username); + assertNull(store.getPreference(username, prefKey)); + newStore(); + assertNull(store.getPreference(username, prefKey)); + } +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/app/AuthenticationContextImpl.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/app/AuthenticationContextImpl.java index e9ae87755bf..f3f62c416f1 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/app/AuthenticationContextImpl.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/authentication/app/AuthenticationContextImpl.java @@ -11,7 +11,7 @@ import com.sap.sse.security.ui.shared.UserDTO; public class AuthenticationContextImpl implements AuthenticationContext { private final UserDTO currentUser; - private final static UserDTO ANONYMOUS = new UserDTO("Anonymous", "", "", "", false, new ArrayList(), + private final static UserDTO ANONYMOUS = new UserDTO("Anonymous", "", "", "", null, false, new ArrayList(), new ArrayList(), new ArrayList()); /** diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java index 48b0ada4f0a..51707bb9fd0 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java @@ -31,7 +31,7 @@ public interface UserManagementService extends RemoteService { void updateSimpleUserEmail(String username, String newEmail, String validationBaseURL) throws UserManagementException, MailException; - void updateUserProperties(String username, String fullName, String company) throws UserManagementException; + void updateUserProperties(String username, String fullName, String company, String localeName) throws UserManagementException; void resetPassword(String username, String eMailAddress, String baseURL) throws UserManagementException, MailException; diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java index 159a67a00ff..a27a905f451 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java @@ -28,7 +28,7 @@ public interface UserManagementServiceAsync { void updateSimpleUserEmail(String username, String newEmail, String validationBaseURL, AsyncCallback callback); - void updateUserProperties(String username, String fullName, String company, AsyncCallback callback); + void updateUserProperties(String username, String fullName, String company, String localeName, AsyncCallback callback); void getFilteredSortedUserList(String filter, AsyncCallback> callback); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java index a02996c83bc..cd660b0a46e 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java @@ -8,10 +8,13 @@ import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.Random; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; import com.sap.sse.gwt.client.Storage; import com.sap.sse.gwt.client.StorageEvent; import com.sap.sse.gwt.client.StorageEvent.Handler; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.settings.SettingsToJsonSerializerGWT; import com.sap.sse.security.ui.client.i18n.StringMessages; import com.sap.sse.security.ui.oauth.client.util.ClientUtils; import com.sap.sse.security.ui.shared.SuccessInfo; @@ -44,6 +47,8 @@ public class UserService { * and windows about changes in the currently logged-in user. */ private static final String LOCAL_STORAGE_UPDATE_KEY = "current-user-has-changed"; + + private final SettingsToJsonSerializerGWT serializer = new SettingsToJsonSerializerGWT(); private final UserManagementServiceAsync userManagementService; @@ -205,5 +210,87 @@ public class UserService { public UserManagementServiceAsync getUserManagementService() { return userManagementService; } + + /** + * Loads the {@link #getCurrentUser() current user}'s preference with the given {@link String key} from server. + * Because the preference is persisted as JSON on server, the loaded data will be deserialized into the provided + * {@link GenericSerializableSettings} instance. + * + * @param key + * key of the preference to load + * @param emptyInstance + * an empty {@link GenericSerializableSettings} instance, where the loaded data is deserialized to + * @param callback + * {@link AsyncCallback} for GWT RPC call + * + * @see GenericSerializableSettings + * @see AbstractGenericSerializableSettings + */ + public void getPreference(String key, final T emptyInstance, + final AsyncCallback callback) { + String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side + getUserManagementService().getPreference(username, key, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + callback.onFailure(caught); + } + + @Override + public void onSuccess(String result) { + callback.onSuccess(serializer.deserialize(emptyInstance, result)); + } + }); + } + /** + * Sets the {@link #getCurrentUser() current user}'s preference with the given {@link String key} on server. + * Because preferences are persisted as JSON, the provided {@link GenericSerializableSettings} instance + * will be serialized before it is sent to the server. + * + * @param key + * key of the preference to set + * @param instance + * {@link GenericSerializableSettings} instance containing the preference value + * + * @see GenericSerializableSettings + * @see AbstractGenericSerializableSettings + */ + public void setPreference(String key, GenericSerializableSettings instance) { + String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side + String json = serializer.serializeToString(instance); + getUserManagementService().setPreference(username, key, json, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + // TODO What to do in case of failure? + } + + @Override + public void onSuccess(Void result) { + // TODO Do anything in case of success? + } + }); + } + + /** + * Unsets the {@link #getCurrentUser() current user}'s preference with the given key on server. + * + * @param key key of the preference to unset + * + * @see GenericSerializableSettings + * @see AbstractGenericSerializableSettings + */ + public void unsetPreference(String key) { + String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side + getUserManagementService().unsetPreference(username, key, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + // TODO What to do in case of failure? + } + + @Override + public void onSuccess(Void result) { + // TODO Do anything in case of success? + } + }); + } } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.java index 170b284a7dc..2fc3324879b 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.java @@ -98,4 +98,5 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages { String accountConfirmation(); String error(); String refresh(); + String preferredLanguage(); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties index 695f2c8eadf..2fb3130c411 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties @@ -45,7 +45,7 @@ errorUpdatingPermissions=Error updating permissions for user {0}: {1} enterRoleName=Enter role name... enterPermissionName=Enter permission name... errorCreatingUser=Error signing up with username {0}: {1} -signedUpSuccessfully=Successfully signed up user {0}. Please check you e-mails to validate your e-mail account. +signedUpSuccessfully=Successfully signed up user {0}. Please check your e-mails to validate your e-mail account. unknownErrorCreatingUser=Unknown error trying to sign up user with name {0}. Perhaps another user with that name already exists. userAlreadyExists=A user with name {0} already exists currentPassword=Current password @@ -93,4 +93,5 @@ youAreNotSignedIn=You are currently not signed in. youDontHaveRequiredPermission=You don''t have the required permission. accountConfirmation=Account confirmation error=Unknown error -refresh=Refresh \ No newline at end of file +refresh=Refresh +preferredLanguage=Preferred language diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_de.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_de.properties index 961c3654b6a..616fdb0405b 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_de.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_de.properties @@ -93,4 +93,5 @@ youAreNotSignedIn=Sie sind aktuell nicht eingeloggt. youDontHaveRequiredPermission=Sie besitzen nicht die benötigten Rechte. accountConfirmation=Kontobestätigung error=Es ist ein Fehler aufgetreten -refresh=Aktualisieren \ No newline at end of file +refresh=Aktualisieren +preferredLanguage=Bevorzugte Sprache diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java index 5ab5cd037c1..b1ad90064a8 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -166,11 +167,12 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U } @Override - public void updateUserProperties(final String username, String fullName, String company) throws UserManagementException { + public void updateUserProperties(final String username, String fullName, String company, String localeName) throws UserManagementException { final Subject subject = SecurityUtils.getSubject(); // the signed-in subject has role ADMIN or is changing own user if (subject.hasRole(DefaultRoles.ADMIN.getRolename()) || username.equals(subject.getPrincipal().toString())) { - getSecurityService().updateUserProperties(username, fullName, company); + getSecurityService().updateUserProperties(username, fullName, company, + localeName == null || localeName.isEmpty() ? null : Locale.forLanguageTag(localeName)); } else { throw new UserManagementException(UserManagementException.INVALID_CREDENTIALS); } @@ -302,7 +304,8 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U } } userDTO = new UserDTO(user.getName(), user.getEmail(), user.getFullName(), user.getCompany(), - user.isEmailValidated(), accountDTOs, user.getRoles(), user.getPermissions()); + user.getLocale() != null ? user.getLocale().toLanguageTag() : null, user.isEmailValidated(), + accountDTOs, user.getRoles(), user.getPermissions()); return userDTO; } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java index 0711d793807..49f10f1f87b 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/UserDTO.java @@ -16,6 +16,7 @@ public class UserDTO implements IsSerializable { private String email; private String fullName; private String company; + private String locale; private List accounts; private Set roles; private Set permissions; @@ -23,12 +24,13 @@ public class UserDTO implements IsSerializable { UserDTO() {} // for serialization only - public UserDTO(String name, String email, String fullName, String company, boolean emailValidated, + public UserDTO(String name, String email, String fullName, String company, String locale, boolean emailValidated, List accounts, Iterable roles, Iterable stringPermissions) { this.name = name; this.email = email; this.fullName = fullName; this.company = company; + this.locale = locale; this.emailValidated = emailValidated; this.accounts = accounts; this.roles = new HashSet<>(); @@ -50,6 +52,10 @@ public class UserDTO implements IsSerializable { public String getCompany() { return company; } + + public String getLocale() { + return locale; + } public Iterable getRoles() { return roles; diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/desktop/userdetails/UserAccountDetails.ui.xml b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/desktop/userdetails/UserAccountDetails.ui.xml index aaa56199983..4f17c40cb94 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/desktop/userdetails/UserAccountDetails.ui.xml +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/desktop/userdetails/UserAccountDetails.ui.xml @@ -24,12 +24,18 @@ -
    +
    +
    + + +
    diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/mobile/userdetails/UserDetails.ui.xml b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/mobile/userdetails/UserDetails.ui.xml index de781b77064..f848eeb65ff 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/mobile/userdetails/UserDetails.ui.xml +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/mobile/userdetails/UserDetails.ui.xml @@ -25,6 +25,12 @@
    +
    +
    + +
    + +
    diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/AbstractUserDetails.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/AbstractUserDetails.java index 0e49bc6a03a..f8b387fea3d 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/AbstractUserDetails.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/AbstractUserDetails.java @@ -1,13 +1,18 @@ package com.sap.sse.security.ui.userprofile.shared.userdetails; +import java.io.IOException; + import com.google.gwt.dom.client.InputElement; import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.text.shared.Renderer; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.PasswordTextBox; import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.ValueListBox; import com.google.gwt.user.client.ui.Widget; +import com.sap.sse.gwt.client.GWTLocaleUtil; import com.sap.sse.security.ui.client.i18n.StringMessages; import com.sap.sse.security.ui.shared.UserDTO; @@ -23,6 +28,20 @@ public class AbstractUserDetails extends Composite implements UserDetailsView { @UiField public InputElement usernameUi; @UiField public TextBox nameUi; @UiField public TextBox companyUi; + + @UiField(provided = true) + public ValueListBox localeUi = new ValueListBox(new Renderer() { + @Override + public String render(String object) { + return GWTLocaleUtil.getDecoratedLanguageDisplayNameWithDefaultLocaleSupport(object); + } + + @Override + public void render(String object, Appendable appendable) throws IOException { + appendable.append(render(object)); + } + }); + @UiField public TextBox emailUi; @UiField public PasswordTextBox oldPasswordUi; @UiField public PasswordTextBox newPasswordUi; @@ -54,6 +73,10 @@ public class AbstractUserDetails extends Composite implements UserDetailsView { usernameUi.setValue(currentUser.getName()); emailUi.setValue(currentUser.getEmail()); + String currentLocale = currentUser.getLocale(); + localeUi.setValue(currentLocale); + localeUi.setAcceptableValues(GWTLocaleUtil.getAvailableLocalesAndDefault()); + clearPasswordFields(); } @@ -69,7 +92,7 @@ public class AbstractUserDetails extends Composite implements UserDetailsView { @UiHandler("saveChangesUi") public void onSaveChangesClicked(ClickEvent event) { - presenter.handleSaveChangesRequest(nameUi.getValue(), companyUi.getValue()); + presenter.handleSaveChangesRequest(nameUi.getValue(), companyUi.getValue(), localeUi.getValue()); } @UiHandler("changeEmailUi") diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsPresenter.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsPresenter.java index fa50245238b..ae42fe54269 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsPresenter.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsPresenter.java @@ -42,9 +42,9 @@ public class UserDetailsPresenter implements AbstractUserDetails.Presenter { } @Override - public void handleSaveChangesRequest(String fullName, String company) { + public void handleSaveChangesRequest(String fullName, String company, String locale) { final String username = authenticationManager.getAuthenticationContext().getCurrentUser().getName(); - userManagementService.updateUserProperties(username, fullName, company, + userManagementService.updateUserProperties(username, fullName, company, locale, new AsyncCallback() { @Override public void onSuccess(Void result) { diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsView.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsView.java index 1053b8ba5dd..72305b3234d 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsView.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/userprofile/shared/userdetails/UserDetailsView.java @@ -15,7 +15,7 @@ public interface UserDetailsView { void clearPasswordFields(); public interface Presenter { - void handleSaveChangesRequest(String fullName, String company); + void handleSaveChangesRequest(String fullName, String company, String locale); void handleEmailChangeRequest(String email); void handlePasswordChangeRequest(String oldPassword, String newPassword, String newPasswordConfirmation); } diff --git a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EditProfile.gwt.xml b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EditProfile.gwt.xml index 8b8b2e0929f..bfedaf891e1 100755 --- a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EditProfile.gwt.xml +++ b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EditProfile.gwt.xml @@ -14,6 +14,7 @@ --> + diff --git a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EmailValidation.gwt.xml b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EmailValidation.gwt.xml index a8cb13ad687..734eede5c40 100755 --- a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EmailValidation.gwt.xml +++ b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/EmailValidation.gwt.xml @@ -14,6 +14,7 @@ --> + diff --git a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/LoginPanel.gwt.xml b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/LoginPanel.gwt.xml index b2be37e6831..3d2df62e948 100644 --- a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/LoginPanel.gwt.xml +++ b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/LoginPanel.gwt.xml @@ -11,7 +11,9 @@ --> + + diff --git a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/OAuthLogin.gwt.xml b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/OAuthLogin.gwt.xml index 5f64a3b98f4..1560b67199e 100644 --- a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/OAuthLogin.gwt.xml +++ b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/OAuthLogin.gwt.xml @@ -16,6 +16,7 @@ --> + diff --git a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/Register.gwt.xml b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/Register.gwt.xml index 83d302198c9..554473fbc13 100644 --- a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/Register.gwt.xml +++ b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/Register.gwt.xml @@ -12,6 +12,7 @@ --> + diff --git a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/UserManagement.gwt.xml b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/UserManagement.gwt.xml index 8cc1b6d694c..5ab0b6332b0 100644 --- a/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/UserManagement.gwt.xml +++ b/java/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/UserManagement.gwt.xml @@ -11,6 +11,7 @@ --> + diff --git a/java/com.sap.sse.security.userstore.mongodb/META-INF/MANIFEST.MF b/java/com.sap.sse.security.userstore.mongodb/META-INF/MANIFEST.MF index 12b89a6475f..23dc56c7dcd 100644 --- a/java/com.sap.sse.security.userstore.mongodb/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.security.userstore.mongodb/META-INF/MANIFEST.MF @@ -14,6 +14,7 @@ Require-Bundle: com.sap.sse.security, org.apache.shiro.core;bundle-version="1.2.2", com.sap.sse.mongodb, com.sap.sse.common, - com.sap.sse.security.common + com.sap.sse.security.common, + com.sap.sse.shared.android Export-Package: com.sap.sse.security.userstore.mongodb, com.sap.sse.security.userstore.mongodb.impl;x-friends:="com.sap.sse.security.test" diff --git a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/UserStoreImpl.java b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/UserStoreImpl.java index 574d38ba348..25a4d428111 100644 --- a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/UserStoreImpl.java +++ b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/UserStoreImpl.java @@ -1,16 +1,26 @@ package com.sap.sse.security.userstore.mongodb; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; import java.util.logging.Logger; import org.apache.shiro.SecurityUtils; +import com.sap.sse.common.Util; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.security.PreferenceConverter; +import com.sap.sse.security.PreferenceObjectListener; import com.sap.sse.security.SocialSettingsKeys; import com.sap.sse.security.User; import com.sap.sse.security.UserStore; @@ -48,6 +58,29 @@ public class UserStoreImpl implements UserStore { */ private final ConcurrentHashMap> preferences; + /** + * Converter objects to map preference Strings to Objects. + * The keys must match the keys of the preferences. + */ + private final ConcurrentHashMap> preferenceConverters; + + /** + * This is another view of the String preferences mapped by {@link #preferenceConverters} to Objects. + * Keys are the usernames, values are the key/value pairs representing the user's preferences. + */ + private final ConcurrentHashMap> preferenceObjects; + + /** + * Keys are preferences keys as used by {@link #preferenceObjects}, values are the listeners to inform on changes of + * the specific preference object for a {@link User}. + */ + private transient Map>> listeners; + + /** + * To be used for locking when working with {@link #listeners}. + */ + private transient NamedReentrantReadWriteLock listenersLock; + /** * Won't be serialized and remains null on the de-serializing end. */ @@ -65,6 +98,11 @@ public class UserStoreImpl implements UserStore { settingTypes = new ConcurrentHashMap<>(); usersByAccessToken = new ConcurrentHashMap<>(); preferences = new ConcurrentHashMap<>(); + preferenceConverters = new ConcurrentHashMap<>(); + preferenceObjects = new ConcurrentHashMap<>(); + listeners = new HashMap<>(); + listenersLock = new NamedReentrantReadWriteLock( + UserStoreImpl.class.getSimpleName() + " lock for listeners collection", false); this.mongoObjectFactory = mongoObjectFactory; if (domainObjectFactory != null) { for (Entry> e : domainObjectFactory.loadSettingTypes().entrySet()) { @@ -101,10 +139,23 @@ public class UserStoreImpl implements UserStore { } } } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + listeners = new HashMap<>(); + listenersLock = new NamedReentrantReadWriteLock( + UserStoreImpl.class.getSimpleName() + " lock for listeners collection", false); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } @Override public void clear() { preferences.clear(); + preferenceConverters.clear(); + clearAllPreferenceObjects(); emailForUsername.clear(); settings.clear(); settingTypes.clear(); @@ -113,6 +164,18 @@ public class UserStoreImpl implements UserStore { usersByAccessToken.clear(); } + /** + * Preference objects can't be simply removed by clearing {@link #preferenceObjects} because listeners can have a + * state depending on the current preference objects. So we need to notify all listeners about the removal of the + * notification objects. + */ + private void clearAllPreferenceObjects() { + final Set usersToProcess = new HashSet<>(preferences.keySet()); + for (String username : usersToProcess) { + removeAllPreferenceObjectsForUser(username); + } + } + @Override public void replaceContentsFrom(UserStore newUserStore) { clear(); @@ -284,7 +347,7 @@ public class UserStoreImpl implements UserStore { result = null; } else { Set set = usersByEmail.get(email); - if (set.isEmpty()) { + if (set == null || set.isEmpty()) { result = null; } else { result = set.iterator().next(); @@ -365,6 +428,7 @@ public class UserStoreImpl implements UserStore { mongoObjectFactory.deleteUser(users.get(name)); } removeFromUsersByEmail(users.remove(name)); + removeAllPreferencesForUser(name); } @Override @@ -405,6 +469,11 @@ public class UserStoreImpl implements UserStore { @Override public void setPreference(String username, String key, String value) { + setPreferenceInternal(username, key, value); + updatePreferenceObjectIfConverterIsAvailable(username, key); + } + + private void setPreferenceInternal(String username, String key, String value) { Map userMap = preferences.get(username); if (userMap == null) { synchronized (preferences) { @@ -431,6 +500,7 @@ public class UserStoreImpl implements UserStore { mongoObjectFactory.storePreferences(username, userMap); } } + unsetPreferenceObject(username, key); } @Override @@ -457,6 +527,29 @@ public class UserStoreImpl implements UserStore { return result; } + private void removeAllPreferencesForUser(String username) { + // TODO should we keep the preferences anonymized (e.g. use a UUID as username) to enable better statistics? + synchronized (preferences) { + preferences.remove(username); + } + if (mongoObjectFactory != null) { + mongoObjectFactory.storePreferences(username, Collections.emptyMap()); + } + removeAllPreferenceObjectsForUser(username); + } + + private void removeAllPreferenceObjectsForUser(String username) { + Map preferenceObjectsToRemove; + synchronized (preferenceObjects) { + preferenceObjectsToRemove = preferenceObjects.remove(username); + } + if(preferenceObjectsToRemove != null) { + for(Map.Entry entry: preferenceObjectsToRemove.entrySet()) { + notifyListenersOnPreferenceObjectChange(username, entry.getKey(), entry.getValue(), null); + } + } + } + @Override public Map getAllSettings() { return settings; @@ -466,4 +559,170 @@ public class UserStoreImpl implements UserStore { public Map> getAllSettingTypes() { return settingTypes; } + + @Override + public void registerPreferenceConverter(String preferenceKey, PreferenceConverter converter) { + PreferenceConverter alreadyAssociatedConverter = preferenceConverters.putIfAbsent(preferenceKey, converter); + + if (alreadyAssociatedConverter == null) { + final Set usersToProcess = new HashSet<>(preferences.keySet()); + for (String user : usersToProcess) { + updatePreferenceObjectWithConverter(user, preferenceKey, converter); + } + } else { + logger.log(Level.SEVERE, "PreferenceConverter " + alreadyAssociatedConverter + " for key " + preferenceKey + + " is already registered. Converter " + converter + " will not be registered"); + } + } + + @Override + public void removePreferenceConverter(String preferenceKey) { + PreferenceConverter preferenceConverterToRemove = preferenceConverters.remove(preferenceKey); + if (preferenceConverterToRemove != null) { + final Set usersToProcess = new HashSet<>(preferences.keySet()); + for (String username : usersToProcess) { + unsetPreferenceObject(username, preferenceKey); + } + } else { + logger.log(Level.WARNING, "PreferenceConverter for key " + preferenceKey + + " should be removed but wasn't registered"); + } + + } + + private void updatePreferenceObjectIfConverterIsAvailable(String username, String key) { + PreferenceConverter preferenceConverter = preferenceConverters.get(key); + if (preferenceConverter != null) { + updatePreferenceObjectWithConverter(username, key, preferenceConverter); + } + } + + private void updatePreferenceObjectWithConverter(String username, String key, PreferenceConverter preferenceConverter) { + final String preferenceString = getPreference(username, key); + if (preferenceString != null) { + try { + final Object convertedObject = preferenceConverter.toPreferenceObject(preferenceString); + setPreferenceObjectInternal(username, key, convertedObject); + } catch (Throwable t) { + logger.log(Level.SEVERE, "Error while converting preference for key " + key + " from String \"" + + preferenceString + "\"", t); + } + } + } + + private void setPreferenceObjectInternal(String username, String key, final Object convertedObject) { + Map userMap = preferenceObjects.get(username); + if (userMap == null) { + synchronized (preferenceObjects) { + // only synchronize when necessary + userMap = preferenceObjects.get(username); + if (userMap == null) { + userMap = new ConcurrentHashMap<>(); + preferenceObjects.put(username, userMap); + } + } + } + // if the new preference object is simply null, we remove the entry instead of putting null + Object oldPreference = convertedObject == null ? userMap.remove(key) : userMap.put(key, convertedObject); + if (oldPreference != null || convertedObject != null) { + // preference hasn't changed if it was null and is now null + notifyListenersOnPreferenceObjectChange(username, key, oldPreference, convertedObject); + } + } + + private void unsetPreferenceObject(String username, String key) { + Map userObjectMap = preferenceObjects.get(username); + if (userObjectMap != null) { + Object oldPreference = userObjectMap.remove(key); + if(oldPreference != null) { + notifyListenersOnPreferenceObjectChange(username, key, oldPreference, null); + } + } + } + + @Override + public T getPreferenceObject(String username, String key) { + final Object result; + Map userMap = preferenceObjects.get(username); + if (userMap != null) { + result = userMap.get(key); + } else { + result = null; + } + @SuppressWarnings("unchecked") + T resultT = (T) result; + return resultT; + } + + @Override + public void setPreferenceObject(String username, String key, Object preferenceObject) + throws IllegalArgumentException { + @SuppressWarnings("unchecked") + PreferenceConverter preferenceConverter = (PreferenceConverter) preferenceConverters.get(key); + if (preferenceConverter == null) { + throw new IllegalArgumentException("Setting preference for key "+key+" but there is no converter associated!"); + } + if (preferenceObject == null) { + unsetPreference(username, key); + } else { + try { + String stringPreference = preferenceConverter.toPreferenceString(preferenceObject); + setPreferenceInternal(username, key, stringPreference); + setPreferenceObjectInternal(username, key, preferenceObject); + } catch (Throwable t) { + logger.log(Level.SEVERE, "Error while converting preference for key " + key + " from Object \"" + + preferenceObject + "\"", t); + } + } + } + + private void notifyListenersOnPreferenceObjectChange(String username, String key, Object oldPreference, + Object newPreference) { + LockUtil.lockForRead(listenersLock); + try { + for (PreferenceObjectListener listener : Util.get(listeners, key, + Collections.> emptySet())) { + @SuppressWarnings("unchecked") + PreferenceObjectListener listenerToFire = (PreferenceObjectListener) listener; + listenerToFire.preferenceObjectChanged(username, key, oldPreference, newPreference); + } + } finally { + LockUtil.unlockAfterRead(listenersLock); + } + } + + @Override + public void addPreferenceObjectListener(String key, PreferenceObjectListener listener, + boolean fireForAlreadyExistingPreferences) { + LockUtil.lockForWrite(listenersLock); + try { + Util.addToValueSet(listeners, key, listener); + if (fireForAlreadyExistingPreferences) { + final Set usersToProcess = new HashSet<>(preferences.keySet()); + for (String username : usersToProcess) { + Map userMap = preferenceObjects.get(username); + if (userMap != null) { + Object preferenceObject = userMap.get(key); + if (preferenceObject != null) { + @SuppressWarnings("unchecked") + PreferenceObjectListener listenerToFire = (PreferenceObjectListener) listener; + listenerToFire.preferenceObjectChanged(username, key, null, preferenceObject); + } + } + } + } + } finally { + LockUtil.unlockAfterWrite(listenersLock); + } + } + + @Override + public void removePreferenceObjectListener(PreferenceObjectListener listener) { + LockUtil.lockForWrite(listenersLock); + try { + Util.removeFromAllValueSets(listeners, listener); + } finally { + LockUtil.unlockAfterWrite(listenersLock); + } + } } diff --git a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/Activator.java b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/Activator.java index 1bf1b5eca8e..9226a1b243c 100644 --- a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/Activator.java +++ b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/Activator.java @@ -7,6 +7,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.security.PreferenceConverterRegistrationManager; import com.sap.sse.security.UserStore; import com.sap.sse.security.userstore.mongodb.UserStoreImpl; @@ -14,6 +15,7 @@ public class Activator implements BundleActivator { private static BundleContext context; private ServiceRegistration registration; + private PreferenceConverterRegistrationManager preferenceConverterRegistrationManager; static BundleContext getContext() { return context; @@ -26,8 +28,10 @@ public class Activator implements BundleActivator { */ public void start(BundleContext bundleContext) throws Exception { Activator.context = bundleContext; + UserStoreImpl userStore = new UserStoreImpl(); registration = context.registerService(UserStore.class.getName(), - new UserStoreImpl(), null); + userStore, null); + preferenceConverterRegistrationManager = new PreferenceConverterRegistrationManager(bundleContext, userStore); Logger.getLogger(Activator.class.getName()).info("User store registered."); for (CollectionNames name : CollectionNames.values()) { MongoDBService.INSTANCE.registerExclusively(CollectionNames.class, name.name()); @@ -40,6 +44,7 @@ public class Activator implements BundleActivator { * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext bundleContext) throws Exception { + preferenceConverterRegistrationManager.stop(); registration.unregister(); Activator.context = null; } diff --git a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/DomainObjectFactoryImpl.java b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/DomainObjectFactoryImpl.java index 921b99b44d1..07e0487fc49 100644 --- a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/DomainObjectFactoryImpl.java @@ -3,6 +3,7 @@ package com.sap.sse.security.userstore.mongodb.impl; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -66,6 +67,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { final String email = (String) userDBObject.get(FieldNames.User.EMAIL.name()); final String fullName = (String) userDBObject.get(FieldNames.User.FULLNAME.name()); final String company = (String) userDBObject.get(FieldNames.User.COMPANY.name()); + final String localeRaw = (String) userDBObject.get(FieldNames.User.LOCALE.name()); + final Locale locale = localeRaw != null ? Locale.forLanguageTag(localeRaw) : null; Boolean emailValidated = (Boolean) userDBObject.get(FieldNames.User.EMAIL_VALIDATED.name()); String passwordResetSecret = (String) userDBObject.get(FieldNames.User.PASSWORD_RESET_SECRET.name()); String validationSecret = (String) userDBObject.get(FieldNames.User.VALIDATION_SECRET.name()); @@ -85,7 +88,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { } DBObject accountsMap = (DBObject) userDBObject.get(FieldNames.User.ACCOUNTS.name()); Map accounts = createAccountMapFromdDBObject(accountsMap); - User result = new User(name, email, fullName, company, emailValidated==null?false:emailValidated, passwordResetSecret, validationSecret, accounts.values()); + User result = new User(name, email, fullName, company, locale, emailValidated==null?false:emailValidated, passwordResetSecret, validationSecret, accounts.values()); for (String role : roles) { result.addRole(role); } diff --git a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/FieldNames.java b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/FieldNames.java index 2827246a87a..6951a7d65dd 100644 --- a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/FieldNames.java +++ b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/FieldNames.java @@ -6,6 +6,7 @@ public class FieldNames { NAME, FULLNAME, COMPANY, + LOCALE, EMAIL, ACCOUNTS, ROLES, PERMISSIONS, diff --git a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/MongoObjectFactoryImpl.java b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/MongoObjectFactoryImpl.java index 9a70a948896..eadfc41af1d 100644 --- a/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sse.security.userstore.mongodb/src/com/sap/sse/security/userstore/mongodb/impl/MongoObjectFactoryImpl.java @@ -40,6 +40,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { dbUser.put(FieldNames.User.EMAIL.name(), user.getEmail()); dbUser.put(FieldNames.User.FULLNAME.name(), user.getFullName()); dbUser.put(FieldNames.User.COMPANY.name(), user.getCompany()); + dbUser.put(FieldNames.User.LOCALE.name(), user.getLocale() != null ? user.getLocale().toLanguageTag() : null); dbUser.put(FieldNames.User.EMAIL_VALIDATED.name(), user.isEmailValidated()); dbUser.put(FieldNames.User.PASSWORD_RESET_SECRET.name(), user.getPasswordResetSecret()); dbUser.put(FieldNames.User.VALIDATION_SECRET.name(), user.getValidationSecret()); diff --git a/java/com.sap.sse.security/META-INF/MANIFEST.MF b/java/com.sap.sse.security/META-INF/MANIFEST.MF index d4b4e63e3d4..486da0880bc 100644 --- a/java/com.sap.sse.security/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.security/META-INF/MANIFEST.MF @@ -33,7 +33,8 @@ Require-Bundle: javax.servlet;bundle-version="2.5.0", com.sap.sse.operationaltransformation, com.sap.sse.security.common, com.sap.sse, - com.sap.sse.mail + com.sap.sse.mail, + com.sap.sse.shared.android Web-ContextPath: /security Export-Package: com.sap.sse.security, com.sap.sse.security.jaxrs diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceConverter.java b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceConverter.java new file mode 100644 index 00000000000..68e4c31db5e --- /dev/null +++ b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceConverter.java @@ -0,0 +1,22 @@ +package com.sap.sse.security; + +/** + * Converter to be used by {@link UserStore} to convert preference objects to/from Strings. + * + * @see UserStore#registerPreferenceConverter(String, PreferenceConverter) + * + * @param + * The type of preference object, this converter handles. + */ +public interface PreferenceConverter { + /** + * To be used when publishing {@link PreferenceConverter} instances to the OSGi service registry. A property with + * this key needs to be added to the publication that defines the preference key for which the specific converter + * instance should be registered at the {@link UserStore}. + */ + public final String KEY_PARAMETER_NAME = "key"; + + String toPreferenceString(PREF preference); + + PREF toPreferenceObject(String stringPreference); +} diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceConverterRegistrationManager.java b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceConverterRegistrationManager.java new file mode 100644 index 00000000000..5dc7b2afd52 --- /dev/null +++ b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceConverterRegistrationManager.java @@ -0,0 +1,88 @@ +package com.sap.sse.security; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +import com.sap.sse.common.Stoppable; + +/** + * Automatically registers/removes {@link PreferenceConverter} instances found in the OSGi service registry on the given + * {@link UserStore}. + *

    + * + * {@link PreferenceConverter} found are required to have a property with the key + * {@link PreferenceConverter#KEY_PARAMETER_NAME} defining the preference key the converter should be registered with. + */ +public class PreferenceConverterRegistrationManager implements Stoppable { + private static final Logger logger = Logger.getLogger(PreferenceConverterRegistrationManager.class.getName()); + + private final BundleContext context; + private final UserStore userStore; + + private final ServiceTracker, PreferenceConverter> tracker; + + /** + * @param bundleContext + * the {@link BundleContext} needed to track {@link PreferenceConverter} instances that need to be + * registered a the given UserStore. + * @param userStore + * the {@link UserStore} to register {@link PreferenceConverter} at. + */ + public PreferenceConverterRegistrationManager(BundleContext bundleContext, UserStore userStore) { + this.context = bundleContext; + this.userStore = userStore; + tracker = createTracker(bundleContext); + tracker.open(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private ServiceTracker, PreferenceConverter> createTracker(BundleContext bundleContext) { + return new ServiceTracker, PreferenceConverter>(bundleContext, + (Class) PreferenceConverter.class, new Cutomizer()); + } + + @Override + public void stop() { + tracker.close(); + } + + private class Cutomizer implements ServiceTrackerCustomizer, PreferenceConverter> { + + @Override + public PreferenceConverter addingService(ServiceReference> reference) { + final String preferenceKey = (String) reference.getProperty(PreferenceConverter.KEY_PARAMETER_NAME); + final PreferenceConverter converter = context.getService(reference); + if (preferenceKey == null) { + logger.log(Level.SEVERE, + "Found PreferenceConverter \"" + converter + "\" in OSGi registry without property " + + PreferenceConverter.KEY_PARAMETER_NAME + " defining the preference key"); + } else { + logger.log(Level.FINE, "Registering PreferenceConverter \"" + converter + + "\" found as OSGi service with key \"" + preferenceKey + "\""); + userStore.registerPreferenceConverter(preferenceKey, converter); + } + return converter; + } + + @Override + public void modifiedService(ServiceReference> reference, + PreferenceConverter service) { + // Should we do anything here? the preference key could have changed, but does this make any sense? + } + + @Override + public void removedService(ServiceReference> reference, PreferenceConverter service) { + String preferenceKey = (String) reference.getProperty(PreferenceConverter.KEY_PARAMETER_NAME); + if (preferenceKey != null) { + logger.log(Level.FINE, + "Removing PreferenceConverter \"" + service + "\" with key \"" + preferenceKey + "\""); + userStore.removePreferenceConverter(preferenceKey); + } + } + } +} diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceObjectBasedNotificationSet.java b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceObjectBasedNotificationSet.java new file mode 100644 index 00000000000..6555e5951c9 --- /dev/null +++ b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceObjectBasedNotificationSet.java @@ -0,0 +1,212 @@ +package com.sap.sse.security; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +import com.sap.sse.common.Stoppable; +import com.sap.sse.common.Util; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; + +/** + *

    + * Preferences available in the {@link UserStore} are held by the user as key. The preferences are stored as complex + * objects that are being serialized to String to be saved in the DB. For convenience, the {@link UserStore} allows to + * register {@link PreferenceConverter}s to also hold a deserialized version of the preferences in memory to speed up + * the access without the need of a deserialization on demand. + *

    + * + *

    + * When coming from the notification perspective, we have a domain object (e.g. competitor) and would like to know the + * users to notify about this domain object. To calculate this Set of users we would need to loop over the specific + * notification preferences for all users. + *

    + * + *

    + * To speed up this part, the {@link PreferenceObjectBasedNotificationSet} holds an optimized association of who to notify about + * specific objects. On preference changes for the given key in the given {@link UserStore}, the associations are being + * updated to make the model always reflect the current state of the associations. Due to this the calculation of the + * users to notify is a simple lookup. + *

    + * + *

    + * To use this, implement a subclass in which the {@link #calculateObjectsToNotify(Object)} method determines the domain + * objects from a preference object for which the user needs to be notified. + *

    + * + * @param + * The type of preference object on which the calculation of the objects to notify is based on. + * @param + * The type of object for which the users to notify is managed. + */ +public abstract class PreferenceObjectBasedNotificationSet implements Stoppable { + private static final Logger logger = Logger.getLogger(PreferenceObjectBasedNotificationSet.class.getName()); + + private UserStore store; + private final PreferenceObjectListener listener = new PreferenceObjectListenerImpl(); + + /** + * The keys are the objects to notify for. The values are the names of the users to notify about an object. + */ + private final Map> notifications = new HashMap<>(); + + private final BundleContext context; + + private final String key; + + private final ServiceTracker tracker; + + private final NamedReentrantReadWriteLock lock; + + /** + * Constructor used to automatically track {@link UserStore} as OSGi service. + */ + public PreferenceObjectBasedNotificationSet(String key, BundleContext context) { + this.key = key; + this.context = context; + if (context == null) { + this.tracker = null; + } else { + this.tracker = new ServiceTracker(context, UserStore.class, new Cutomizer()); + this.tracker.open(); + } + this.lock = new NamedReentrantReadWriteLock(getClass().getName()+" for "+key, /* fair */ false); + } + + /** + * Constructor used to directly work with a given {@link UserStore}. + */ + public PreferenceObjectBasedNotificationSet(String key, UserStore store) { + this(key, /* context */ (BundleContext) null); + this.store = store; + store.addPreferenceObjectListener(key, listener, true); + } + + @Override + public void stop() { + if (tracker != null) { + tracker.close(); + } + removeStore(); + } + + private void removeStore() { + if (store != null) { + store.removePreferenceObjectListener(listener); + store = null; + } + LockUtil.lockForWrite(lock); + try { + notifications.clear(); + } finally { + LockUtil.unlockAfterWrite(lock); + } + } + + /** + * Determines the domain objects for which the user needs to be notified. + * + * @param preference + * a notification preference of some sort which typically describes or contains a set of domain objects + * the user is "interested" in and for changes of which the user wants to be notified. + * @return the domain objects described by the {@code preference} object thta the user is "interested" in + */ + protected abstract Collection calculateObjectsToNotify(PrefT preference); + + public Iterable getUsersnamesToNotifyFor(T object) { + LockUtil.lockForRead(lock); + try { + return new HashSet<>(Util.get(notifications, object, Collections.emptySet())); + } finally { + LockUtil.unlockAfterRead(lock); + } + } + + /** + * The given consumer will be called for every user that needs to be notified about the given object. + * Users without a verified email address will be skipped. + */ + public void forUsersWithVerifiedEmailMappedTo(T object, Consumer consumer) { + for (String username : getUsersnamesToNotifyFor(object)) { + // User objects can change silently. So we just keep the usernames and get the associated user objects on + // the fly. + User user = store.getUserByName(username); + if (user == null) { + logger.log(Level.SEVERE, "Could not get User for name \"" + username + "\""); + } else { + // we can not exclude users without verified email address when calculating the association because this + // state could have changed meanwhile. So this need to be done on the fly. + if(user.isEmailValidated()) { + consumer.accept(user); + } + } + } + } + + private class PreferenceObjectListenerImpl implements PreferenceObjectListener { + @Override + public void preferenceObjectChanged(String username, String key, PrefT oldPreference, PrefT newPreference) { + Collection oldObjectsToNotify = oldPreference == null ? Collections.emptySet() + : calculateObjectsToNotify(oldPreference); + Collection newObjectsToNotify = newPreference == null ? Collections.emptySet() + : calculateObjectsToNotify(newPreference); + + Set objectsToRemove = new HashSet<>(oldObjectsToNotify); + objectsToRemove.removeAll(newObjectsToNotify); + + Set objectsToAdd = new HashSet<>(newObjectsToNotify); + objectsToAdd.removeAll(oldObjectsToNotify); + + LockUtil.lockForWrite(lock); + try { + for (T objectToRemove : objectsToRemove) { + Util.removeFromValueSet(notifications, objectToRemove, username); + } + for (T objectToAdd : objectsToAdd) { + Util.addToValueSet(notifications, objectToAdd, username); + } + } finally { + LockUtil.unlockAfterWrite(lock); + } + } + } + + private class Cutomizer implements ServiceTrackerCustomizer { + @Override + public UserStore addingService(ServiceReference reference) { + UserStore store = context.getService(reference); + if (PreferenceObjectBasedNotificationSet.this.store != null + && PreferenceObjectBasedNotificationSet.this.store != store) { + logger.severe("Multiple " + UserStore.class.getSimpleName() + + " instances found. Only one instance is handled."); + } else { + PreferenceObjectBasedNotificationSet.this.store = store; + store.addPreferenceObjectListener(key, listener, true); + } + return store; + } + + @Override + public void modifiedService(ServiceReference reference, UserStore service) { + } + + @Override + public void removedService(ServiceReference reference, UserStore service) { + if (PreferenceObjectBasedNotificationSet.this.store == service) { + removeStore(); + } + } + } +} diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceObjectListener.java b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceObjectListener.java new file mode 100644 index 00000000000..fa7ac835822 --- /dev/null +++ b/java/com.sap.sse.security/src/com/sap/sse/security/PreferenceObjectListener.java @@ -0,0 +1,25 @@ +package com.sap.sse.security; + +/** + * Listener used by UserStore to notify about changes of stored preference objects. + * + * @param + * The type of preference object this Listener handles. + */ +public interface PreferenceObjectListener { + /** + * Called when the preference object for the given user and preference key has changed. + * + * @param username + * the user for whom the preference object changed + * @param key + * the key of the changed preference object + * @param oldPreference + * the old state of the preference object. May be null if there was not preference object + * formerly. + * @param newPreference + * the new state of the preference object. May be null if the preference object is being + * deleted. + */ + void preferenceObjectChanged(String username, String key, PrefT oldPreference, PrefT newPreference); +} diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java index d9e34b171ef..7382c68cda6 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java @@ -1,5 +1,6 @@ package com.sap.sse.security; +import java.util.Locale; import java.util.Map; import java.util.UUID; @@ -57,7 +58,7 @@ public interface SecurityService extends ReplicableWithObjectInputStream accounts) { - this(name, email, /* fullName */ null, /* company */ null, /* is email validated */ false, + this(name, email, /* fullName */ null, /* company */ null, /* locale */ null, /* is email validated */ false, /* password reset secret */ null, /* validation secret */ null, accounts); } - public User(String name, String email, String fullName, String company, Boolean emailValidated, + public User(String name, String email, String fullName, String company, Locale locale, Boolean emailValidated, String passwordResetSecret, String validationSecret, Collection accounts) { super(); this.name = name; this.fullName = fullName; this.company = company; + this.locale = locale; this.roles = new HashSet<>(); this.permissions = new HashSet<>(); this.email = email; @@ -119,6 +127,18 @@ public class User implements NamedWithID { public void setCompany(String company) { this.company = company; } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public Locale getLocaleOrDefault() { + return locale == null ? Locale.ENGLISH : locale; + } public Iterable getRoles() { return roles; @@ -243,7 +263,8 @@ public class User implements NamedWithID { @Override public String toString() { - return "User [name=" + name + ", email=" + email + (isEmailValidated()?" (validated)":")")+", roles=" + return "User [name=" + name + ", email=" + email + ", fullName=" + fullName + ", company=" + company + + ", locale=" + locale + (isEmailValidated() ? " (validated)" : ")") + ", roles=" + Arrays.toString(roles.toArray(new String[roles.size()])) + ", accounts=" + Arrays.toString(accounts.keySet().toArray(new AccountType[accounts.size()])) + "]"; } diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/UserStore.java b/java/com.sap.sse.security/src/com/sap/sse/security/UserStore.java index f51bcb8423b..5fbe8618b17 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/UserStore.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/UserStore.java @@ -11,8 +11,14 @@ import com.sap.sse.security.shared.UserManagementException; public interface UserStore extends Named { Iterable getUsers(); + /** + * The user with that {@link User#getName() name} or {@code null} if no such user exists + */ User getUserByName(String name); + /** + * The user with that {@link User#getEmail() email} or {@code null} if no such user exists + */ User getUserByEmail(String email); User getUserByAccessToken(String accessToken); @@ -53,6 +59,56 @@ public interface UserStore extends Named { void unsetPreference(String username, String key); String getPreference(String username, String key); + + /** + *

    + * In an OSGi environment, this shouldn't be called manually, but instead automatically managed by setting a + * {@link PreferenceConverterRegistrationManager} up. {@link PreferenceConverter}s should be registered in the OSGi + * service registry with {@link PreferenceConverter#KEY_PARAMETER_NAME} containing the associated preference key + * added as property of the service registration. + *

    + * + *

    + * Registers a converter objects for a preference key that is used to convert preference Strings to Objects. This + * makes it possible to access deserialized settings without the need to do the deserialization over and over again. + *

    + * + * @param key + * the key to associate the converter with + * @param converter + * the converter to use for (de)serialization + */ + void registerPreferenceConverter(String key, PreferenceConverter converter); + + /** + *

    + * In an OSGi environment, this shouldn't be called manually, but instead automatically managed by setting a + * {@link PreferenceConverterRegistrationManager} up. + *

    + * + *

    + * Removes a registered {@link PreferenceConverter} with the given key. + *

    + * + * @param key + * the key of the {@link PreferenceConverter} to remove + */ + void removePreferenceConverter(String key); + + /** + * Gets a preference object. Always returns null if there is no converter associated with the given key -> see + * {@link #registerPreferenceConverter(String, PreferenceConverter)}. + */ + T getPreferenceObject(String username, String key); + + /** + * Sets a preference as Object. This converts the given Object to a preference String using a + * {@link PreferenceConverter} that was registered through + * {@link #registerPreferenceConverter(String, PreferenceConverter)}. + * + * @throws IllegalArgumentException if there is no {@link PreferenceConverter} registered with the given key. + */ + void setPreferenceObject(String username, String key, Object preferenceObject) throws IllegalArgumentException; /** * Sets a value for a key if that key was previously added to this store using {@link #addSetting(String, Class)}. @@ -80,6 +136,7 @@ public interface UserStore extends Named { /** * Removes all users and all their preferences and all settings from this store's in-memory representation. * For safety reasons and because a replica's DB state is undefined anyhow, leaves persistent content in place. + * Registered listeners will not be removed automatically. * Use with due care. */ void clear(); @@ -106,4 +163,8 @@ public interface UserStore extends Named { * {@code username} no access token has previously been {@link #setAccessToken(String, String) set}. */ String getAccessToken(String username); + + void addPreferenceObjectListener(String key, PreferenceObjectListener listener, boolean fireForAlreadyExistingPreferences); + + void removePreferenceObjectListener(PreferenceObjectListener listener); } diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java b/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java index a4e6375a4e3..8d9cf45c929 100755 --- a/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java @@ -88,7 +88,7 @@ public class Activator implements BundleActivator { try { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); // ensure that classpath:... Shiro ini files are resolved properly logger.info("Waiting for UserStore service..."); - UserStore userStore = tracker.waitForService(0); + final UserStore userStore = tracker.waitForService(0); logger.info("Obtained UserStore service "+userStore); createAndRegisterSecurityService(bundleContext, userStore); } catch (InterruptedException e) { diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java index 042a8c2d407..66098b25d9a 100755 --- a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java @@ -11,6 +11,7 @@ import java.net.URLEncoder; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -391,17 +392,18 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat } @Override - public void updateUserProperties(String username, String fullName, String company) throws UserManagementException { + public void updateUserProperties(String username, String fullName, String company, Locale locale) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } - updateUserProperties(user, fullName, company); + updateUserProperties(user, fullName, company, locale); } - private void updateUserProperties(User user, String fullName, String company) { + private void updateUserProperties(User user, String fullName, String company, Locale locale) { user.setFullName(fullName); user.setCompany(company); + user.setLocale(locale); apply(s->s.internalStoreUser(user)); } diff --git a/java/com.sap.sse.shared.android.test/.classpath b/java/com.sap.sse.shared.android.test/.classpath new file mode 100644 index 00000000000..eca7bdba8f0 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sse.shared.android.test/.gitignore b/java/com.sap.sse.shared.android.test/.gitignore new file mode 100644 index 00000000000..743bbf5b315 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/.gitignore @@ -0,0 +1,2 @@ +bin +/bin/ diff --git a/java/com.sap.sse.shared.android.test/.project b/java/com.sap.sse.shared.android.test/.project new file mode 100644 index 00000000000..a32b5afbd2d --- /dev/null +++ b/java/com.sap.sse.shared.android.test/.project @@ -0,0 +1,28 @@ + + + com.sap.sse.shared.android.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sse.shared.android.test/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sse.shared.android.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..a698e59674f --- /dev/null +++ b/java/com.sap.sse.shared.android.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sse.shared.android.test/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sse.shared.android.test/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..f29e940a005 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sse.shared.android.test/META-INF/MANIFEST.MF b/java/com.sap.sse.shared.android.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..40e6dcd56b5 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Tests for SSE Shared Android +Bundle-SymbolicName: com.sap.sse.shared.android.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Fragment-Host: com.sap.sse.shared.android +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.mockito.mockito-core;bundle-version="1.9.5", + org.hamcrest;bundle-version="1.1.0", + org.objenesis;bundle-version="1.3.0", + org.junit4;bundle-version="4.8.2" +Export-Package: com.sap.sse.shared.android.test diff --git a/java/com.sap.sse.shared.android.test/build.properties b/java/com.sap.sse.shared.android.test/build.properties new file mode 100644 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sse.shared.android.test/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sse.shared.android.test/pom.xml b/java/com.sap.sse.shared.android.test/pom.xml new file mode 100644 index 00000000000..b3e8f6a5910 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sse.shared.android.test + eclipse-test-plugin + diff --git a/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/AbstractSettingsSerializationTest.java b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/AbstractSettingsSerializationTest.java new file mode 100644 index 00000000000..40b8d468a9d --- /dev/null +++ b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/AbstractSettingsSerializationTest.java @@ -0,0 +1,225 @@ +package com.sap.sse.shared.android.test; + +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import java.util.Arrays; + +import org.junit.Test; + +import com.sap.sse.common.filter.TextOperator; +import com.sap.sse.common.filter.TextOperator.Operators; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.BooleanSetting; +import com.sap.sse.common.settings.generic.DecimalListSetting; +import com.sap.sse.common.settings.generic.DecimalSetting; +import com.sap.sse.common.settings.generic.EnumListSetting; +import com.sap.sse.common.settings.generic.EnumSetting; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsList; +import com.sap.sse.common.settings.generic.StringSetting; +import com.sap.sse.common.settings.generic.StringToEnumConverter; + +public abstract class AbstractSettingsSerializationTest { + + private static class TestOuterSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -7379232503773525915L; + private transient SimpleTestSettings nested; + + public TestOuterSettings() { + } + + @Override + protected void addChildSettings() { + nested = new SimpleTestSettings("nested", this); + } + } + + private static class TestListSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = 6919127895749914961L; + private transient SettingsList l; + + public TestListSettings() { + } + + @Override + protected void addChildSettings() { + l = new SettingsList<>("l", this, SimpleTestSettings::new); + } + } + + private static class SimpleTestSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -4134836978411240572L; + private transient StringSetting string; + private transient DecimalSetting num; + + public SimpleTestSettings() { + } + + public SimpleTestSettings(String string, BigDecimal num) { + this.string.setValue(string); + this.num.setValue(num); + } + + public SimpleTestSettings(String name, AbstractGenericSerializableSettings parent) { + super(name, parent); + } + + @Override + protected void addChildSettings() { + string = new StringSetting("string", this); + num = new DecimalSetting("num", this); + } + } + + private static class TestSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -611806711715538293L; + private transient StringSetting humba; + private transient BooleanSetting bumpa; + private transient EnumSetting trala; + private transient DecimalSetting num; + private transient DecimalListSetting l; + + public TestSettings() { + } + + @Override + protected void addChildSettings() { + humba = new StringSetting("humba", this); + bumpa = new BooleanSetting("bumpa", this); + trala = new EnumSetting<>("trala", this, TextOperator.Operators::valueOf); + num = new DecimalSetting("num", this); + l = new DecimalListSetting("l", this); + } + } + + private static class TestEnumListSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = 93688955681544920L; + private transient EnumListSetting l; + + public TestEnumListSettings() { + } + + @Override + protected void addChildSettings() { + l = new EnumListSetting<>("l", this, + new StringToEnumConverter() { + @Override + public Operators fromString(String stringValue) { + return TextOperator.Operators.valueOf(stringValue); + } + }); + } + } + + private static class DuplicateFieldSettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = 4058775568295038177L; + @SuppressWarnings("unused") + private transient StringSetting humba; + @SuppressWarnings("unused") + private transient DecimalSetting bumba; + + @Override + protected void addChildSettings() { + humba = new StringSetting("humba", this); + bumba = new DecimalSetting("humba", this); + } + } + + private static class DisallowedKeySettings extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -2265305217290147424L; + @SuppressWarnings("unused") + private transient StringSetting disallowedKey; + + @Override + protected void addChildSettings() { + disallowedKey= new StringSetting("disallowed.key", this); + } + } + + protected abstract SOT serialize(T settings) throws Exception; + + protected T deserialize(SOT serializedObject, Class settingsClass) throws Exception { + T deserializedInstance = settingsClass.newInstance(); + + return deserialize(serializedObject, deserializedInstance); + } + + protected T deserialize(SOT serializedObject, T settings) throws Exception { + return null; + }; + + @SuppressWarnings("unchecked") + private T serializeAndDeserialize(T objectToSerialize) { + try { + SOT serialized = serialize(objectToSerialize); + return deserialize(serialized, (Class) objectToSerialize.getClass()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testFlatJsonSerialization() { + final TestSettings settings = new TestSettings(); + settings.humba.setValue("trala"); + settings.bumpa.setValue(true); + settings.trala.setValue(TextOperator.Operators.Contains); + settings.num.setValue(BigDecimal.TEN); + settings.l.setValues(Arrays.asList(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3))); + + final TestSettings deserializedSettings = serializeAndDeserialize(settings); + assertEquals(settings, deserializedSettings); + } + + @Test + public void testNestedJsonSerialization() { + final TestOuterSettings outerSettings = new TestOuterSettings(); + final SimpleTestSettings settings = outerSettings.nested; + settings.string.setValue("trala"); + settings.num.setValue(BigDecimal.TEN); + + final TestOuterSettings deserializedSettings = serializeAndDeserialize(outerSettings); + assertEquals(outerSettings, deserializedSettings); + } + + /** + * Verifies that it is not possible to have two equally named child settings that would cause conflicts on + * serialization. + */ + @Test(expected = RuntimeException.class) + public void testDuplicateSetting() { + new DuplicateFieldSettings(); + } + + /** + * Verifies that it is not possible to have setting names with ".". + */ + @Test(expected = RuntimeException.class) + public void testDisallowedSettingNameSetting() { + new DisallowedKeySettings(); + } + + /** + * A white-box test that tests property name escaping in case the property name conflicts with how type names (used + * particularly for enum settings) are represented in JSON. + */ + @Test + public void testJsonSerializationWithEnumTypedList() { + final TestEnumListSettings settings = new TestEnumListSettings(); + settings.l.setValues(Arrays.asList(Operators.Contains, Operators.EndsWith)); + + final TestEnumListSettings deserializedSettings = serializeAndDeserialize(settings); + assertEquals(settings, deserializedSettings); + } + + @Test + public void testJsonSerializationWithNestedSettingsList() { + final TestListSettings settings = new TestListSettings(); + settings.l.setValues(Arrays.asList(new SimpleTestSettings("blubb", BigDecimal.TEN), new SimpleTestSettings( + "bla", BigDecimal.ONE))); + + final TestListSettings deserializedSettings = serializeAndDeserialize(settings); + assertEquals(settings, deserializedSettings); + } +} diff --git a/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsJavaSerializationTest.java b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsJavaSerializationTest.java new file mode 100644 index 00000000000..598c1469ac8 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsJavaSerializationTest.java @@ -0,0 +1,32 @@ +package com.sap.sse.shared.android.test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import com.sap.sse.common.settings.generic.GenericSerializableSettings; + +public class SettingsJavaSerializationTest extends AbstractSettingsSerializationTest { + + @Override + protected byte[] serialize(T settings) throws Exception { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos);) { + oos.writeObject(settings); + oos.close(); + return baos.toByteArray(); + } + + } + + @SuppressWarnings("unchecked") + @Override + protected T deserialize(byte[] serializedObject, Class settingsClass) throws Exception { + try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedObject); + ObjectInputStream ois = new ObjectInputStream(bais);) { + return (T) ois.readObject(); + } + + } +} diff --git a/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsToJsonSerializationTest.java b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsToJsonSerializationTest.java new file mode 100644 index 00000000000..d39895fe4f9 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsToJsonSerializationTest.java @@ -0,0 +1,21 @@ +package com.sap.sse.shared.android.test; + +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.shared.settings.SettingsToJsonSerializer; + +public class SettingsToJsonSerializationTest extends AbstractSettingsSerializationTest { + + private final SettingsToJsonSerializer serializer = new SettingsToJsonSerializer(); + + @Override + protected String serialize(T settings) throws Exception { + // Do not change this to return a JSONOBject instance. + // JSONOBject is just a HashMap and you only know if it is really JSON serializable if you do it! + return serializer.serializeToString(settings); + } + + @Override + protected T deserialize(String serializedObject, T settings) throws Exception { + return serializer.deserialize(settings, serializedObject); + } +} diff --git a/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsToStringMapSerializationTest.java b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsToStringMapSerializationTest.java new file mode 100644 index 00000000000..877fe13fa75 --- /dev/null +++ b/java/com.sap.sse.shared.android.test/src/com/sap/sse/shared/android/test/SettingsToStringMapSerializationTest.java @@ -0,0 +1,21 @@ +package com.sap.sse.shared.android.test; + +import java.util.Map; + +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.serializer.SettingsToStringMapSerializer; + +public class SettingsToStringMapSerializationTest extends AbstractSettingsSerializationTest>> { + + private final SettingsToStringMapSerializer serializer = new SettingsToStringMapSerializer(); + + @Override + protected Map> serialize(T settings) throws Exception { + return serializer.serialize(settings); + } + + @Override + protected T deserialize(Map> serializedObject, T settings) throws Exception { + return serializer.deserialize(settings, serializedObject); + } +} diff --git a/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF b/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF index 179cf981850..3ac90edcb68 100644 --- a/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF @@ -5,8 +5,15 @@ Bundle-SymbolicName: com.sap.sse.shared.android Bundle-Version: 1.0.0.qualifier Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.7 -Require-Bundle: com.sap.sse.common +Require-Bundle: com.sap.sse.common, + org.json.simple;bundle-version="1.1.0" Export-Package: com.sap.sse.concurrent, com.sap.sse.shared.media, com.sap.sse.shared.media.impl, + com.sap.sse.shared.settings, com.sap.sse.util.impl +Import-Package: com.sap.sse.common.settings, + com.sap.sse.common.settings.generic, + com.sap.sse.common.settings.generic.base, + com.sap.sse.common.settings.generic.converter, + com.sap.sse.common.settings.serializer diff --git a/java/com.sap.sse.shared.android/src/com/sap/sse/shared/settings/SettingsToJsonSerializer.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/settings/SettingsToJsonSerializer.java new file mode 100644 index 00000000000..4634ac9ec64 --- /dev/null +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/settings/SettingsToJsonSerializer.java @@ -0,0 +1,68 @@ +package com.sap.sse.shared.settings; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import com.sap.sse.common.settings.Settings; +import com.sap.sse.common.settings.generic.GenericSerializableSettings; +import com.sap.sse.common.settings.serializer.AbstractSettingsToJsonSerializer; + +/** + * Serializes a {@link Settings} object to a {@link JSONObject}. All {@link Settings} subclasses are supported as top-level entities. + * Nesting of {@link Settings} is generally supported. + * + * @author Axel Uhl (D043530) + * + */ +public class SettingsToJsonSerializer extends AbstractSettingsToJsonSerializer { + public String serializeToString(GenericSerializableSettings settings) { + return serialize(settings).toJSONString(); + } + + public T deserialize(T settings, String jsonString) { + if (jsonString != null && !jsonString.isEmpty()) { + try { + deserialize(settings, (JSONObject) new JSONParser().parse(jsonString)); + } catch (ParseException e) { + throw new RuntimeException("Could not parse settings as JSON: " + jsonString, e); + } + } + return settings; + } + + @Override + protected JSONObject newOBJECT() { + return new JSONObject(); + } + + @Override + protected void set(JSONObject jsonObject, String property, Object value) { + jsonObject.put(property, value); + } + + @Override + protected Object get(JSONObject jsonObject, String property) { + return jsonObject.get(property); + } + + @Override + protected boolean hasProperty(JSONObject jsonObject, String property) { + return jsonObject.containsKey(property); + } + + @Override + protected JSONArray ToJsonArray(Iterable values) { + JSONArray jsonArray = new JSONArray(); + for(Object value : values) { + jsonArray.add(value); + } + return jsonArray; + } + + @Override + protected Iterable fromJsonArray(JSONArray jsonArray) { + return jsonArray; + } +} diff --git a/java/com.sap.sse/META-INF/MANIFEST.MF b/java/com.sap.sse/META-INF/MANIFEST.MF index c1d1ed3529f..3adeecda510 100644 --- a/java/com.sap.sse/META-INF/MANIFEST.MF +++ b/java/com.sap.sse/META-INF/MANIFEST.MF @@ -11,7 +11,6 @@ Export-Package: com.sap.sse, com.sap.sse.i18n.impl, com.sap.sse.osgi, com.sap.sse.qrcode, - com.sap.sse.settings, com.sap.sse.util, com.sap.sse.util.impl, com.sap.sse.util.kmeans diff --git a/java/com.sap.sse/src/com/sap/sse/settings/SettingsToJsonSerializer.java b/java/com.sap.sse/src/com/sap/sse/settings/SettingsToJsonSerializer.java deleted file mode 100755 index c5e3adb5344..00000000000 --- a/java/com.sap.sse/src/com/sap/sse/settings/SettingsToJsonSerializer.java +++ /dev/null @@ -1,223 +0,0 @@ -package com.sap.sse.settings; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; - -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.common.settings.EnumSetting; -import com.sap.sse.common.settings.ListSetting; -import com.sap.sse.common.settings.MapSettings; -import com.sap.sse.common.settings.NumberSetting; -import com.sap.sse.common.settings.Setting; -import com.sap.sse.common.settings.SettingType; -import com.sap.sse.common.settings.Settings; -import com.sap.sse.common.settings.StringSetting; - -/** - * Serializes a {@link Settings} object to a {@link JSONObject}. All setting types are supported as top-level entities. - * Nesting of {@link Settings} is generally supported. For example, a {@link Settings} object can be contains as one - * setting within another {@link Settings} object. However, when nesting {@link EnumSetting} objects within a - * {@link ListSetting} then it is mandatory to put this {@link ListSetting} object into a {@link Settings} object's - * {@link Settings#getNonDefaultSettings()}, such as into a {@link MapSettings} object. The technical reason for this - * limitation is that the {@link Class} object for the enumeration type must be stored with the serialized format in - * order to re-construct the enumeration literals of the correct type. For readability of the produced JSON output, - * this implementation chooses to store the enumeration class's name in a special property that is sibling of the - * enumeration property. A {@link ListSetting} object does not contain named properties and therefore does not allow - * us to add such a property easily. - * - * @author Axel Uhl (D043530) - * - */ -public class SettingsToJsonSerializer { - private final static String TYPE_PROPERTY_SUFFIX = "___TYPE"; - private final static char TYPE_PROPERTY_SUFFIX_ESCAPE = ':'; - - public JSONObject serialize(Settings settings) { - final Map settingsToSerialize = settings.getNonDefaultSettings(); - JSONObject jsonObject = new JSONObject(); - for (Entry e : settingsToSerialize.entrySet()) { - Object settingAsJson = serialize(e.getValue()); - jsonObject.put(escapePropertyName(e.getKey()), settingAsJson); - final String typePropertyName = getTypePropertyName(e.getKey()); - if (e.getValue() instanceof EnumSetting) { - jsonObject.put(typePropertyName, getEnumTypeName((EnumSetting) e.getValue())); - } else if (e.getValue() instanceof ListSetting) { - Iterator i = ((ListSetting) e.getValue()).iterator(); - if (i.hasNext()) { - Object o = i.next(); - if (o instanceof EnumSetting) { - jsonObject.put(typePropertyName, getEnumListTypeName(((EnumSetting) o).getValue())); - } - } - } - } - return jsonObject; - } - - /** - * A list containing enum literals shall be represented as simply holding these literals as strings. However, - * when de-serializing, the list elements need to be constructed as the proper enum's literals again, so we need - * to store the enumeration class name together with the information that it's a list. - */ - private Object getEnumListTypeName(Enum literal) { - return SettingType.LIST.name()+"/"+literal.getClass().getName(); - } - - private Object getEnumTypeName(EnumSetting value) { - return SettingType.ENUM.name()+"/"+value.getValue().getClass().getName(); - } - - private String getTypePropertyName(String unescapedKey) { - return unescapedKey+TYPE_PROPERTY_SUFFIX; - } - - private boolean isTypePropertyName(String unescapedKey) { - return unescapedKey.endsWith(TYPE_PROPERTY_SUFFIX) && !unescapedKey.endsWith(TYPE_PROPERTY_SUFFIX_ESCAPE+TYPE_PROPERTY_SUFFIX); - } - - /** - * If key looks like a result of {@link #getTypePropertyName(String)}, the string is escaped such that - * it does not more and that {@link #unescapePropertyName} will return key.

    - */ - String escapePropertyName(String key) { - return key.replaceAll(TYPE_PROPERTY_SUFFIX, TYPE_PROPERTY_SUFFIX_ESCAPE+TYPE_PROPERTY_SUFFIX); - } - - String unescapePropertyName(String escapedKey) { - return escapedKey.replaceAll(TYPE_PROPERTY_SUFFIX_ESCAPE+TYPE_PROPERTY_SUFFIX, TYPE_PROPERTY_SUFFIX); - } - - private Object serialize(Setting setting) { - switch (setting.getType()) { - case ENUM: - return serialize((EnumSetting) setting); - case LIST: - return serialize((ListSetting) setting); - case MAP: - return serialize((Settings) setting); - case NUMBER: - return serialize((NumberSetting) setting); - case STRING: - return serialize((StringSetting) setting); - default: - throw new IllegalArgumentException("Don't know setting of type "+setting.getType()); - } - } - - private > String serialize(EnumSetting enumSetting) { - return enumSetting.getValue().name(); - } - - private JSONArray serialize(ListSetting listSetting) { - JSONArray result = new JSONArray(); - for (T t : listSetting) { - result.add(serialize(t)); - } - return result; - } - - private Number serialize(NumberSetting numberSetting) { - return numberSetting.getNumber(); - } - - private String serialize(StringSetting stringSetting) { - return stringSetting.getString(); - } - - public Settings deserialize(JSONObject json) throws ClassNotFoundException { - final Map settings = new HashMap<>(); - for (Entry e : json.entrySet()) { - String escapedKey = (String) e.getKey(); - String unescapedKey = unescapePropertyName(escapedKey); - if (!isTypePropertyName(escapedKey)) { - final Object value = e.getValue(); - String typePropertyName = getTypePropertyName(unescapedKey); - // all properties must specify their type unless they are of type String, JSONArray or Number; - // however, properties of type enum will be represented as strings - final SettingType type; - final Class clazz; - if (json.containsKey(typePropertyName)) { - String[] typeNameAndClassName = ((String) json.get(typePropertyName)).split("/"); - type = SettingType.valueOf(typeNameAndClassName[0]); - if (typeNameAndClassName.length > 1) { - clazz = Class.forName(typeNameAndClassName[1]); - } else { - clazz = null; - } - } else { - type = getSettingType(value); - clazz = null; - } - Setting setting = createSettingFromObjectAndType(value, type, clazz); - settings.put(unescapePropertyName(escapedKey), setting); - } - } - return new AbstractSettings() { - @Override - public Map getNonDefaultSettings() { - return settings; - } - }; - } - - private SettingType getSettingType(final Object value) { - final SettingType type; - if (value instanceof String) { - type = SettingType.STRING; - } else if (value instanceof Number){ - type = SettingType.NUMBER; - } else if (value instanceof JSONArray) { - type = SettingType.LIST; - } else if (value instanceof JSONObject) { - type = SettingType.MAP; - } else { - throw new IllegalArgumentException("Don't know how to de-serialize setting "+value+" of type "+value.getClass()); - } - return type; - } - - private Setting createSettingFromObjectAndType(Object obj, final SettingType type, Class clazz) - throws ClassNotFoundException { - final Setting setting; - switch (type) { - case ENUM: - Enum value = null; - @SuppressWarnings("unchecked") - Class> enumClass = (Class>) clazz; - for (Enum literal : enumClass.getEnumConstants()) { - if (literal.name().equals((String) obj)) { - value = literal; - } - } - setting = new EnumSetting<>(value); - break; - case LIST: - JSONArray array = (JSONArray) obj; - List settingList = new ArrayList<>(); - for (Object o : array) { - settingList.add(createSettingFromObjectAndType(o, clazz==null?getSettingType(o):SettingType.ENUM, clazz)); - } - setting = new ListSetting<>(settingList); - break; - case MAP: - setting = deserialize((JSONObject) obj); - break; - case NUMBER: - setting = new NumberSetting((Number) obj); - break; - case STRING: - setting = new StringSetting((String) obj); - break; - default: - throw new IllegalArgumentException("Don't know how to de-serialize setting "+obj+" of type "+obj.getClass()); - } - return setting; - } -} diff --git a/java/pom.xml b/java/pom.xml index 861856c858a..fe3aa41fe4e 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -1,526 +1,527 @@ - - - 4.0.0 - - - workspace - com.sap.sailing - 1.0.0-SNAPSHOT - - - root - pom - - - - com.sap.sse.common - com.sap.sse.shared.android - com.sap.sse.datamining.annotations - com.sap.sse.security.common - org.json.simple - com.sap.sailing.domain.common - com.sap.sailing.domain.shared.android - com.sap.sailing.server.gateway.serialization.shared.android - - - - 0.21.0 - 2.7.0 - 2.7.0 - 2 - race-analysis-p2-remote - - - -Dfile.encoding=cp1252 -Dmongo.dbName=winddbTest -DspatialWind=true - - - -ea -Xmx1024m -XX:+UseG1GC - - - -Dtractrac.tunnel=true -Dtractrac.tunnel.host=localhost - - - -Dmongo.host=127.0.0.1 - - - -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y - - - - -Dhttp.proxyHost=proxy.wdf.sap.corp -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.wdf.sap.corp -Dhttps.proxyPort=8080 - - ${project.build.directory}/surefire-reports - false - - - - - - with-not-android-relevant - - - !ldi.releaseBuild - - - - com.sap.sse - com.sap.sse.mail - com.sap.sse.mail.replication.test - com.sap.sse.mail.replication.testsupport - com.sap.sse.mongodb - com.sap.sse.gwt - com.sap.sse.gwt.adminconsole - com.sap.sse.datamining - com.sap.sse.datamining.shared - com.sap.sse.security - com.sap.sse.security.ui - com.sap.sse.security.userstore.mongodb - com.sap.sse.operationaltransformation - com.sap.sse.replication - com.sap.sse.threadmanager - com.sap.sse.filestorage - com.sap.sailing.domain - com.sap.sailing.geocoding - com.sap.sailing.datamining - com.sap.sailing.datamining.shared - com.sap.sailing.domain.persistence - com.sap.sailing.domain.deckmanadapter - com.sap.sailing.domain.tractracadapter - com.sap.sailing.domain.tractracadapter.persistence - com.sap.sailing.domain.igtimiadapter - com.sap.sailing.domain.igtimiadapter.persistence - com.sap.sailing.domain.racelogtrackingadapter - com.sap.sailing.domain.racelogtrackingadapter.testsupport - com.sap.sailing.domain.swisstimingadapter - com.sap.sailing.domain.swisstimingadapter.persistence - com.sap.sailing.domain.swisstimingreplayadapter - com.sap.sailing.news - com.sap.sailing.declination - com.sap.sailing.udpconnector - com.sap.sailing.expeditionconnector - com.sap.sailing.server - com.sap.sailing.server.gateway - com.sap.sailing.server.gateway.serialization - com.sap.sailing.www - com.sap.sailing.xmlexport - com.google.gwt.dev - com.google.gwt.osgi - com.tractrac.clientmodule - com.googlecode.java-diff-utils - com.googlecode.mgwt - com.sap.sailing.dashboards.gwt - com.sap.sailing.feature - org.moxieapps.gwt.highcharts - com.sap.sailing.resultimport - com.sap.sailing.kiworesultimport - com.sap.sailing.ess40.resultimport - com.sap.sailing.xrr.schema - com.sap.sailing.xrr.resultimport - com.sap.sailing.xrr.structureimport - com.sap.sailing.freg.resultimport - org.openqa.selenium.osgi - com.sap.sailing.monitoring - com.sap.sailing.simulator - com.sap.sailing.server.trackfiles - com.sap.sailing.feature.runtime - com.sap.sailing.gwt.ui - com.sap.sailing.domain.test - com.sap.sse.replication.testsupport - com.sap.sailing.server.testsupport - - com.sap.sse.gwt.test - - com.sap.sse.datamining.test - com.sap.sse.security.replication.test - com.sap.sse.security.test - com.sap.sse.operationaltransformation.test - com.sap.sse.common.test - com.sap.sse.filestorage.test - com.sap.sailing.datamining.test - com.sap.sailing.domain.deckmanadapter.test - com.sap.sailing.domain.igtimiadapter.test - com.sap.sailing.domain.racelogtrackingadapter.test - com.sap.sailing.xmlexport.test - com.sap.sailing.xrr.resultimport.test - com.sap.sailing.domain.shared.android.test - com.sap.sailing.geocoding.test - com.sap.sailing.domain.swisstimingadapter.test - com.sap.sailing.domain.swisstimingreplayadapter.test - com.sap.sailing.declination.test - com.sap.sailing.expeditionconnector.test - com.sap.sailing.server.test - com.sap.sailing.server.gateway.test - com.sap.sailing.server.gateway.test.support - com.sap.sailing.server.gateway.serialization.test - com.sap.sailing.server.replication.test - com.sap.sailing.gwt.ui.test - com.sap.sailing.mongodb.test - com.googlecode.java-diff-utils.test - com.sap.sailing.kiworesultimport.test - com.sap.sailing.ess40.resultimport.test - com.sap.sailing.freg.resultimport.test - com.sap.sailing.barbados.resultimport - com.sap.sailing.barbados.resultimport.test - com.sap.sailing.velum.resultimport - com.sap.sailing.velum.resultimport.test - com.sap.sailing.sailwave.resultimport - com.sap.sailing.sailwave.resultimport.test - com.sap.sailing.manage2sail - com.sap.sailing.manage2sail.resultimport - com.sap.sailing.manage2sail.resultimport.test - com.sap.sailing.yachtscoring.resultimport - com.sap.sailing.yachtscoring.resultimport.test - com.sap.sailing.simulator.test - com.sap.sailing.server.trackfiles.test - com.sap.sailing.selenium.test - com.sap.sailing.autoload - com.sap.sailing.feature.p2build - com.sap.sailing.polars - com.sap.sailing.polars.datamining - com.sap.sailing.polars.datamining.shared - com.sap.sailing.polars.test - - - - - - org.eclipse.tycho - target-platform-configuration - - - - - eclipse-plugin - com.google.gwt.dev - 0.0.0 - - - - - - - - - - no-debug.with-proxy - - true - - - - ${parameters.common} ${parameters.vm-settings} ${parameters.proxy} ${parameters.tractrac} ${parameters.mongodb} - - - - - no-debug.without-proxy - - - ${parameters.common} ${parameters.vm-settings} ${parameters.mongodb} - - - - - debug.with-proxy - - - ${parameters.common} ${parameters.vm-settings} ${parameters.proxy} ${parameters.debug} ${parameters.mongodb} - - - - - debug.without-proxy - - - ${parameters.common} ${parameters.vm-settings} ${parameters.debug} ${parameters.mongodb} - - - - - p2-target.local - - - race-analysis-p2-local - - - - - p2-local - - - - - - leandi - - - - ldi.releaseBuild - - - - - race-analysis-p2-empty - - - - - com.sap.sailing - install-android-sdk - ${project.version} - provided - pom - - - - - - org.eclipse.tycho - tycho-compiler-plugin - - - 1.7 - 1.7 - - - - - - - - non-leandi - - - - !ldi.releaseBuild - - - - - - - org.eclipse.tycho - tycho-compiler-plugin - - BREE - - - - - - - - - bin - bin - bin - - - - - true - org.eclipse.tycho - eclipse-test-plugin - ${tycho-version} - - - org.apache.maven.plugins - maven-surefire-plugin - 2.16 - - ${reportsdirectory} - - - - com.simpligility.maven.plugins - android-maven-plugin - - src - AndroidManifest.xml - - *.jar - generated-sources/** - maven-archiver/** - sourcebundle-l10n-gen/** - p2* - MANIFEST.MF - *.ap_ - local-artifacts.properties - **/*.gwt.xml - - - - - aar - - aar - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 1.9.1 - - - package - - attach-artifact - - - - - aar - ${project.build.directory}/${project.build.finalName}.aar - - - - - - - - - - - - org.eclipse.tycho - tycho-maven-plugin - ${tycho-version} - true - - - org.eclipse.tycho - target-platform-configuration - ${tycho-version} - - p2 - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ../com.sap.sailing.targetplatform/definitions/${p2-target} - - - - - linux - gtk - x86 - - - linux - gtk - x86_64 - - - win32 - win32 - x86 - - - win32 - win32 - x86_64 - - - macosx - cocoa - x86_64 - - - - - - org.eclipse.tycho - tycho-surefire-plugin - ${tycho-version} - - ${tycho.build.argLine} - - - **/*$* - - ${reportsdirectory} - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - - - - org.codehaus.mojo - gwt-maven-plugin - ${gwt.version} - - - - - -Dgwt.persistentunitcache=false -Xmx2048m -Djava.io.tmpdir=${env.TMP} - - - ${gwt.workers} - - - - - - - - - - - - org.apache.maven.wagon - wagon-ssh-external - 1.0-beta-6 - - - - - - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.16 - - true - - ${reportsdirectory} - true - - - - - - + + + 4.0.0 + + + workspace + com.sap.sailing + 1.0.0-SNAPSHOT + + + root + pom + + + + com.sap.sse.common + com.sap.sse.shared.android + com.sap.sse.datamining.annotations + com.sap.sse.security.common + org.json.simple + com.sap.sailing.domain.common + com.sap.sailing.domain.shared.android + com.sap.sailing.server.gateway.serialization.shared.android + + + + 0.21.0 + 2.7.0 + 2.7.0 + 2 + race-analysis-p2-remote + + + -Dfile.encoding=cp1252 -Dmongo.dbName=winddbTest -DspatialWind=true + + + -ea -Xmx1024m -XX:+UseG1GC + + + -Dtractrac.tunnel=true -Dtractrac.tunnel.host=localhost + + + -Dmongo.host=127.0.0.1 + + + -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y + + + + -Dhttp.proxyHost=proxy.wdf.sap.corp -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.wdf.sap.corp -Dhttps.proxyPort=8080 + + ${project.build.directory}/surefire-reports + false + + + + + + with-not-android-relevant + + + !ldi.releaseBuild + + + + com.sap.sse + com.sap.sse.mail + com.sap.sse.mail.replication.test + com.sap.sse.mail.replication.testsupport + com.sap.sse.mongodb + com.sap.sse.gwt + com.sap.sse.gwt.adminconsole + com.sap.sse.datamining + com.sap.sse.datamining.shared + com.sap.sse.security + com.sap.sse.security.ui + com.sap.sse.security.userstore.mongodb + com.sap.sse.operationaltransformation + com.sap.sse.replication + com.sap.sse.threadmanager + com.sap.sse.filestorage + com.sap.sailing.domain + com.sap.sailing.geocoding + com.sap.sailing.datamining + com.sap.sailing.datamining.shared + com.sap.sailing.domain.persistence + com.sap.sailing.domain.deckmanadapter + com.sap.sailing.domain.tractracadapter + com.sap.sailing.domain.tractracadapter.persistence + com.sap.sailing.domain.igtimiadapter + com.sap.sailing.domain.igtimiadapter.persistence + com.sap.sailing.domain.racelogtrackingadapter + com.sap.sailing.domain.racelogtrackingadapter.testsupport + com.sap.sailing.domain.swisstimingadapter + com.sap.sailing.domain.swisstimingadapter.persistence + com.sap.sailing.domain.swisstimingreplayadapter + com.sap.sailing.news + com.sap.sailing.declination + com.sap.sailing.udpconnector + com.sap.sailing.expeditionconnector + com.sap.sailing.server + com.sap.sailing.server.gateway + com.sap.sailing.server.gateway.serialization + com.sap.sailing.www + com.sap.sailing.xmlexport + com.google.gwt.dev + com.google.gwt.osgi + com.tractrac.clientmodule + com.googlecode.java-diff-utils + com.googlecode.mgwt + com.sap.sailing.dashboards.gwt + com.sap.sailing.feature + org.moxieapps.gwt.highcharts + com.sap.sailing.resultimport + com.sap.sailing.kiworesultimport + com.sap.sailing.ess40.resultimport + com.sap.sailing.xrr.schema + com.sap.sailing.xrr.resultimport + com.sap.sailing.xrr.structureimport + com.sap.sailing.freg.resultimport + org.openqa.selenium.osgi + com.sap.sailing.monitoring + com.sap.sailing.simulator + com.sap.sailing.server.trackfiles + com.sap.sailing.feature.runtime + com.sap.sailing.gwt.ui + com.sap.sailing.domain.test + com.sap.sse.replication.testsupport + com.sap.sailing.server.testsupport + + com.sap.sse.gwt.test + + com.sap.sse.datamining.test + com.sap.sse.security.replication.test + com.sap.sse.security.test + com.sap.sse.operationaltransformation.test + com.sap.sse.common.test + com.sap.sse.shared.android.test + com.sap.sse.filestorage.test + com.sap.sailing.datamining.test + com.sap.sailing.domain.deckmanadapter.test + com.sap.sailing.domain.igtimiadapter.test + com.sap.sailing.domain.racelogtrackingadapter.test + com.sap.sailing.xmlexport.test + com.sap.sailing.xrr.resultimport.test + com.sap.sailing.domain.shared.android.test + com.sap.sailing.geocoding.test + com.sap.sailing.domain.swisstimingadapter.test + com.sap.sailing.domain.swisstimingreplayadapter.test + com.sap.sailing.declination.test + com.sap.sailing.expeditionconnector.test + com.sap.sailing.server.test + com.sap.sailing.server.gateway.test + com.sap.sailing.server.gateway.test.support + com.sap.sailing.server.gateway.serialization.test + com.sap.sailing.server.replication.test + com.sap.sailing.gwt.ui.test + com.sap.sailing.mongodb.test + com.googlecode.java-diff-utils.test + com.sap.sailing.kiworesultimport.test + com.sap.sailing.ess40.resultimport.test + com.sap.sailing.freg.resultimport.test + com.sap.sailing.barbados.resultimport + com.sap.sailing.barbados.resultimport.test + com.sap.sailing.velum.resultimport + com.sap.sailing.velum.resultimport.test + com.sap.sailing.sailwave.resultimport + com.sap.sailing.sailwave.resultimport.test + com.sap.sailing.manage2sail + com.sap.sailing.manage2sail.resultimport + com.sap.sailing.manage2sail.resultimport.test + com.sap.sailing.yachtscoring.resultimport + com.sap.sailing.yachtscoring.resultimport.test + com.sap.sailing.simulator.test + com.sap.sailing.server.trackfiles.test + com.sap.sailing.selenium.test + com.sap.sailing.autoload + com.sap.sailing.feature.p2build + com.sap.sailing.polars + com.sap.sailing.polars.datamining + com.sap.sailing.polars.datamining.shared + com.sap.sailing.polars.test + + + + + + org.eclipse.tycho + target-platform-configuration + + + + + eclipse-plugin + com.google.gwt.dev + 0.0.0 + + + + + + + + + + no-debug.with-proxy + + true + + + + ${parameters.common} ${parameters.vm-settings} ${parameters.proxy} ${parameters.tractrac} ${parameters.mongodb} + + + + + no-debug.without-proxy + + + ${parameters.common} ${parameters.vm-settings} ${parameters.mongodb} + + + + + debug.with-proxy + + + ${parameters.common} ${parameters.vm-settings} ${parameters.proxy} ${parameters.debug} ${parameters.mongodb} + + + + + debug.without-proxy + + + ${parameters.common} ${parameters.vm-settings} ${parameters.debug} ${parameters.mongodb} + + + + + p2-target.local + + + race-analysis-p2-local + + + + + p2-local + + + + + + leandi + + + + ldi.releaseBuild + + + + + race-analysis-p2-empty + + + + + com.sap.sailing + install-android-sdk + ${project.version} + provided + pom + + + + + + org.eclipse.tycho + tycho-compiler-plugin + + + 1.7 + 1.7 + + + + + + + + non-leandi + + + + !ldi.releaseBuild + + + + + + + org.eclipse.tycho + tycho-compiler-plugin + + BREE + + + + + + + + + bin + bin + bin + + + + + true + org.eclipse.tycho + eclipse-test-plugin + ${tycho-version} + + + org.apache.maven.plugins + maven-surefire-plugin + 2.16 + + ${reportsdirectory} + + + + com.simpligility.maven.plugins + android-maven-plugin + + src + AndroidManifest.xml + + *.jar + generated-sources/** + maven-archiver/** + sourcebundle-l10n-gen/** + p2* + MANIFEST.MF + *.ap_ + local-artifacts.properties + **/*.gwt.xml + + + + + aar + + aar + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + package + + attach-artifact + + + + + aar + ${project.build.directory}/${project.build.finalName}.aar + + + + + + + + + + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + p2 + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ../com.sap.sailing.targetplatform/definitions/${p2-target} + + + + + linux + gtk + x86 + + + linux + gtk + x86_64 + + + win32 + win32 + x86 + + + win32 + win32 + x86_64 + + + macosx + cocoa + x86_64 + + + + + + org.eclipse.tycho + tycho-surefire-plugin + ${tycho-version} + + ${tycho.build.argLine} + + + **/*$* + + ${reportsdirectory} + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + + + + org.codehaus.mojo + gwt-maven-plugin + ${gwt.version} + + + + + -Dgwt.persistentunitcache=false -Xmx2048m -Djava.io.tmpdir=${env.TMP} + + + ${gwt.workers} + + + + + + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 1.0-beta-6 + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.16 + + true + + ${reportsdirectory} + true + + + + + + diff --git a/translation.json b/translation.json index f93605340e1..d4abdbd2164 100644 --- a/translation.json +++ b/translation.json @@ -110,6 +110,20 @@ "pseudoLocFolderPath":"." } ] + }, + { + "collectionName":"SailingServer", + "folders":[ + { + "sourceFolderPath":"java/com.sap.sailing.server/resources/stringmessages", + "sourceFilters":[ + "StringMessages*.properties" + ], + "xlfFolderPath":"./XLF", + "targetFolderPath":".", + "pseudoLocFolderPath":"." + } + ] } ] }