Merge branch 'bug4720_mail'

This commit is contained in:
Steffen Schaefer
2019-01-17 13:10:36 +01:00
17 changed files with 457 additions and 193 deletions
@@ -1,10 +1,14 @@
package com.sap.sailing.domain.common;
public class BranchIOConstants {
public static final String SAILINSIGHT_2_APP_BRANCHIO = "https://sailinsight20-app.sapsailing.com/invite";
public static final String SAILINSIGHT_APP_BRANCHIO = "https://sailinsight-app.sapsailing.com/invite";
public static final String SAILINSIGHT_APP_BRANCHIO_PATH = "checkinUrl";
public static final String BUOYPINGER_APP_BRANCHIO = "https://buoypinger-app.sapsailing.com/invite";
public static final String BUOYPINGER_APP_BRANCHIO_PATH = "checkinUrl";
public static final String OPEN_REGATTA_2_APP_BRANCHIO = "https://sailinsight20-app.sapsailing.com/publicInvite";
public static final String OPEN_REGATTA_APP_BRANCHIO = "https://sailinsight-app.sapsailing.com/publicInvite";
public static final String OPEN_REGATTA_APP_BRANCHIO_PATH = "checkinUrl";
private BranchIOConstants() {
};
@@ -23,6 +23,7 @@ public class DeviceMappingConstants {
// all URL components should be treated as case insensitive, preferring _underscores_ to CamelCase.
public static final String TRACKING_URL_BASE = "/tracking/checkin";
public static final String BUOY_TENDER_URL_BASE = "/buoy-tender/checkin";
public static final String URL_CHECKIN_URL = "checkinUrl";
public static final String URL_EVENT_ID = "event_id";
public static final String URL_LEADERBOARD_NAME = "leaderboard_name";
public static final String URL_COMPETITOR_ID_AS_STRING = "competitor_id";
@@ -1,12 +1,15 @@
trackingInvitationFor=Tracking Invitation for
scanQRCodeOrVisitUrlToRegisterAs=Make sure you have the {0} App installed. Then scan the following QRCode or click the link using your smartphone to register as
followBranchDeeplink=Click the following link to start the {0} App and register as <b>{1}</b>:
welcomeTo=Welcome to
iOSUsers=Start iOS App
androidUsers=Start Android App
alternativelyVisitThisLink=If you are reading this mail on the device with the App already installed, simply click below to start:
buoyTender=buoy tender
buoyPingerAppName=SAP Sailing Buoy Pinger
sailInSightAppName=SAP Sail InSight
register=Register
scanQRCodeOrVisitUrlToRegisterAs=Make sure you have the {0} App installed. Then scan the following QRCode or click the link using your smartphone to register as
appStoreInstallText=If you don''t have the App yet, please install:
appIos=Install iOS App
appAndroid=Install Android App
buoyPingerAppName=SAP Sailing Buoy Pinger
sailInSightAppName=SAP Sail InSight
iOSUsers=Start iOS App
androidUsers=Start Android App
alternativelyVisitThisLink=If you are reading this mail on the device with the App already installed, simply click below to start:
@@ -1,12 +1,14 @@
trackingInvitationFor=Einladung zum Tracken für
scanQRCodeOrVisitUrlToRegisterAs=Installiere die {0} App. Dann scanne den QRCode oder klicke den Link mit deinem Smartphone an, um dich zu registrieren als
followBranchDeeplink=Klicke den folgenden Link um die {0} App zu starten und dich zu als <b>{1}</b> zu registrieren:
welcomeTo=Willkommen zu
buoyTender=Bojenleger
buoyPingerAppName=SAP Sailing Buoy Pinger
sailInSightAppName=SAP Sail InSight
register=Registrieren
scanQRCodeOrVisitUrlToRegisterAs=Installiere die {0} App. Dann scanne den QRCode oder klicke den Link mit deinem Smartphone an, um dich zu registrieren als
iOSUsers=iOS App starten
androidUsers=Android App starten
alternativelyVisitThisLink=Wenn die App bereits installiert ist, und du diese Mail auch auf dem Geräte offen hast, dann einfach hier klicken:
buoyTender=Bojenleger
appStoreInstallText=Falls die App noch nicht installiert ist, kannst du sie hier finden:
appIos=iOS App installieren
appAndroid=Android App installieren
buoyPingerAppName=SAP Sailing Buoy Pinger
sailInSightAppName=SAP Sail InSight
sailInSightAppName=SAP Sail InSight
@@ -22,6 +22,7 @@ import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.RegattaLeaderboard;
import com.sap.sailing.domain.racelogtracking.impl.RaceLogRaceTracker;
import com.sap.sailing.domain.tracking.MailInvitationType;
import com.sap.sailing.domain.tracking.RaceHandle;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.server.interfaces.RacingEventService;
@@ -95,7 +96,7 @@ public interface RaceLogTrackingAdapter {
*/
void inviteCompetitorsForTrackingViaEmail(Event event, Leaderboard leaderboard,
String serverUrlWithoutTrailingSlash, Set<Competitor> competitors, String iOSAppUrl, String androidAppUrl,
Locale locale) throws MailException;
Locale locale, MailInvitationType type) throws MailException;
/**
* Invite buoy tenders for buoy pinging via the Buoy Tender App by sending out emails.
@@ -104,7 +105,8 @@ public interface RaceLogTrackingAdapter {
* @throws MailException
*/
void inviteBuoyTenderViaEmail(Event event, Leaderboard leaderboard, String serverUrlWithoutTrailingSlash,
String emails, String iOSAppUrl, String androidAppUrl, Locale locale) throws MailException;
String emails, String iOSAppUrl, String androidAppUrl, Locale locale, MailInvitationType type)
throws MailException;
/**
* Copy the course in the newest {@link RaceLogCourseDesignChangedEvent} in {@code from} race log to the {@code to}
@@ -0,0 +1,23 @@
package com.sap.sailing.domain.racelogtracking.impl;
import java.util.Locale;
import com.sap.sailing.domain.common.BranchIOConstants;
public class BranchIO1RaceLogTrackingInvitationMailBuilder extends BranchIORaceLogTrackingInvitationMailBuilder {
BranchIO1RaceLogTrackingInvitationMailBuilder(Locale locale) {
super(locale);
}
@Override
protected String getSailInsightBranchIO() {
return BranchIOConstants.SAILINSIGHT_APP_BRANCHIO;
}
@Override
protected String getBouyPingerBranchIO() {
return BranchIOConstants.BUOYPINGER_APP_BRANCHIO;
}
}
@@ -0,0 +1,24 @@
package com.sap.sailing.domain.racelogtracking.impl;
import java.util.Locale;
import com.sap.sailing.domain.common.BranchIOConstants;
public class BranchIO2RaceLogTrackingInvitationMailBuilder extends BranchIORaceLogTrackingInvitationMailBuilder {
BranchIO2RaceLogTrackingInvitationMailBuilder(Locale locale) {
super(locale);
}
@Override
protected String getSailInsightBranchIO() {
return BranchIOConstants.SAILINSIGHT_2_APP_BRANCHIO;
}
@Override
protected String getBouyPingerBranchIO() {
// FIXME the app should in the future at some point also support bouy pinging, replace this then
return BranchIOConstants.BUOYPINGER_APP_BRANCHIO;
}
}
@@ -0,0 +1,110 @@
package com.sap.sailing.domain.racelogtracking.impl;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import com.sap.sailing.domain.base.Event;
import com.sap.sailing.domain.common.racelog.tracking.DeviceMappingConstants;
import com.sap.sse.common.media.MediaTagConstants;
import com.sap.sse.shared.media.ImageDescriptor;
import com.sap.sse.util.impl.NonGwtUrlHelper;
/**
* Builder to create invitation mails for competitor and buoy tracking. This class ensures that the mail is being sent
* as text and html body to support a great variety of mail clients.
*/
abstract class BranchIORaceLogTrackingInvitationMailBuilder extends RaceLogTrackingInvitationMailBuilder {
private static final Logger LOG = Logger.getLogger(BranchIORaceLogTrackingInvitationMailBuilder.class.getName());
/**
* @param locale
* the locale in which the resulting mail will be generated. If there are no specific messages available
* for the given locale, English locale is used as fallback.
*/
BranchIORaceLogTrackingInvitationMailBuilder(final Locale locale) {
super(locale);
}
BranchIORaceLogTrackingInvitationMailBuilder addEventLogo(final Event event) {
final List<ImageDescriptor> imagesWithTag = event.findImagesWithTag(MediaTagConstants.LOGO.getName());
if (imagesWithTag != null && !imagesWithTag.isEmpty()) {
final ImageDescriptor imageDescriptor = imagesWithTag.get(0);
final String logoUrl = imageDescriptor.getURL().toString();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream is = new URL(logoUrl).openStream();) {
BufferedImage image = ImageIO.read(is);
ImageIO.write(image, "png", baos);
insertImage(baos.toByteArray(), "logo", logoUrl);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error while getting event image for invitation mail", e);
}
}
return this;
}
@Override
public RaceLogTrackingInvitationMailBuilder addSailInsightDeeplink(final String checkinUrl, String legacy1,
String legacy2) {
String deeplink = String.format("%s?%s=%s", getSailInsightBranchIO(),
DeviceMappingConstants.URL_CHECKIN_URL, NonGwtUrlHelper.INSTANCE.encodeQueryString(checkinUrl));
this.addDeeplink(deeplink, RaceLogTrackingI18n::register);
return this;
}
protected abstract String getSailInsightBranchIO();
@Override
public RaceLogTrackingInvitationMailBuilder addBuoyPingerDeeplink(final String checkinUrl, String legacy1,
String legacy2) {
String deeplink = String.format("%s?%s=%s", getBouyPingerBranchIO(),
DeviceMappingConstants.URL_CHECKIN_URL, NonGwtUrlHelper.INSTANCE.encodeQueryString(checkinUrl));
this.addDeeplink(deeplink, RaceLogTrackingI18n::register);
return this;
}
abstract protected String getBouyPingerBranchIO();
private void addDeeplink(final String url, final Function<Locale, String> textFactory) {
this.html.append("<table border=\"0\" cellspacing=\"20px\" cellpadding=\"0px\">");
this.html.append("<tr>");
this.html.append("<td>");
this.addHtmlLink(url, textFactory);
this.html.append("</td>");
this.addTextLink(url, textFactory);
this.html.append("</tr>");
this.html.append("</table>");
}
@Override
protected void addIntroductoryText(final String appName, final String invitee) {
final String introText = RaceLogTrackingI18n.followBranchDeeplink(locale, appName, invitee);
this.html.append("<p>").append(introText).append("</p>");
this.text.append(introText).append(" ").append(invitee).append(TEXT_LINE_BREAK).append(TEXT_LINE_BREAK);
}
private void addHtmlLink(final String url, final Function<Locale, String> textFactory) {
final String anchor = "<a href=\"" + url + "\" style=\"color:#fff; text-decoration:none;\">";
// outer href for clickablity of the whole button
this.html.append(anchor);
// table being used for the styling, as some mail clients have isssues with divs
this.html.append("<table width=\"100%\" style=\"background-color: #337ab7; color:#fff;\"><tr>");
this.html.append("<td style=\"padding: 15px; text-align: center; color:#fff;\">");
// inner href, so that outlook does work (it does not recognize the outer)
this.html.append(anchor);
this.html.append(textFactory.apply(locale));
this.html.append("</a>");
this.html.append("</td>");
this.html.append("</tr></table>");
this.html.append("</a>");
}
}
@@ -0,0 +1,137 @@
package com.sap.sailing.domain.racelogtracking.impl;
import java.io.DataInputStream;
import java.util.Locale;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sse.qrcode.QRCodeGenerationUtil;
/**
* Builder to create invitation mails for competitor and buoy tracking. This class ensures that the mail is being sent
* as text and html body to support a great variety of mail clients.
*/
class LegacyRaceLogTrackingInvitationMailBuilder extends RaceLogTrackingInvitationMailBuilder {
private static final Logger LOG = Logger.getLogger(LegacyRaceLogTrackingInvitationMailBuilder.class.getName());
/**
* URL prefix, so the iOS app will recognize as deep link and pass anything after it to the app for analysis.
*/
private static final String IOS_DEEP_LINK_PREFIX = "comsapsailingtracker://";
/**
* @param locale
* the locale in which the resulting mail will be generated. If there are no specific messages available
* for the given locale, English locale is used as fallback.
*/
LegacyRaceLogTrackingInvitationMailBuilder(final Locale locale) {
super(locale);
}
LegacyRaceLogTrackingInvitationMailBuilder addQrCodeImage(final String url) {
try (DataInputStream imageIs = new DataInputStream(QRCodeGenerationUtil.create(url, 250))) {
byte[] targetArray = new byte[imageIs.available()];
imageIs.readFully(targetArray);
insertImage(targetArray, "qr", url);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error while generating QR code for invitation mail", e);
}
return this;
}
LegacyRaceLogTrackingInvitationMailBuilder addOpenInAppTextAndLinks(final String targetUrl, final String iOSAppUrl,
final String androidAppUrl) {
final boolean hasIOSAppUrl = iOSAppUrl != null && !iOSAppUrl.isEmpty();
final boolean hasAndroidAppUrl = androidAppUrl != null && !androidAppUrl.isEmpty();
if (hasIOSAppUrl || hasAndroidAppUrl) {
this.html.append("<p>").append(RaceLogTrackingI18n.alternativelyVisitThisLink(this.locale)).append("</p>");
}
this.html.append("<table border=\"0\" cellspacing=\"20px\" cellpadding=\"0px\">");
this.html.append("<tr>");
if (hasIOSAppUrl) {
this.html.append("<td>");
this.addHtmlLink(IOS_DEEP_LINK_PREFIX + targetUrl, RaceLogTrackingI18n::iOSUsers);
this.html.append("</td>");
this.addTextLink(IOS_DEEP_LINK_PREFIX + targetUrl, RaceLogTrackingI18n::iOSUsers);
}
if (hasAndroidAppUrl) {
this.html.append("<td>");
this.addHtmlLink(targetUrl, RaceLogTrackingI18n::androidUsers);
this.html.append("</td>");
this.addTextLink(targetUrl, RaceLogTrackingI18n::androidUsers);
}
this.html.append("</tr>");
this.html.append("</table>");
return this;
}
LegacyRaceLogTrackingInvitationMailBuilder addInstallAppTextAndLinks(final String iOSAppUrl,
final String androidAppUrl) {
final boolean hasIOSAppUrl = iOSAppUrl != null && !iOSAppUrl.isEmpty();
final boolean hasAndroidAppUrl = androidAppUrl != null && !androidAppUrl.isEmpty();
if (hasIOSAppUrl || hasAndroidAppUrl) {
this.html.append("<p>").append(RaceLogTrackingI18n.appStoreInstallText(this.locale)).append("</p>");
text.append(RaceLogTrackingI18n.appStoreInstallText(locale));
text.append(TEXT_LINE_BREAK);
}
this.html.append("<table border=\"0\" cellspacing=\"20px\" cellpadding=\"0px\">");
this.html.append("<tr>");
if (hasIOSAppUrl) {
this.html.append("<td>");
this.addHtmlLink(iOSAppUrl, RaceLogTrackingI18n::appIos);
this.html.append("</td>");
this.addTextLink(iOSAppUrl, RaceLogTrackingI18n::appIos);
}
if (hasAndroidAppUrl) {
this.html.append("<td>");
this.addHtmlLink(androidAppUrl, RaceLogTrackingI18n::appAndroid);
this.html.append("</td>");
this.addTextLink(androidAppUrl, RaceLogTrackingI18n::appAndroid);
}
this.html.append("</tr>");
this.html.append("</table>");
return this;
}
@Override
protected void addIntroductoryText(final String appName, final String invitee) {
final String introText = RaceLogTrackingI18n.scanQRCodeOrVisitUrlToRegisterAs(locale, appName);
this.html.append("<p>").append(introText).append(" <b>").append(invitee).append("</b></p>");
this.text.append(introText).append(" ").append(invitee).append(TEXT_LINE_BREAK).append(TEXT_LINE_BREAK);
}
private void addHtmlLink(final String url, final Function<Locale, String> textFactory) {
final String anchor = "<a href=\"" + url + "\" style=\"color:#fff; text-decoration:none;\">";
// outer href for clickablity of the whole button
this.html.append(anchor);
// table being used for the styling, as some mail clients have isssues with divs
this.html.append("<table width=\"100%\" style=\"background-color: #337ab7; color:#fff;\"><tr>");
this.html.append("<td style=\"padding: 15px; text-align: center; color:#fff;\">");
// inner href, so that outlook does work (it does not recognize the outer)
this.html.append(anchor);
this.html.append(textFactory.apply(locale));
this.html.append("</a>");
this.html.append("</td>");
this.html.append("</tr></table>");
this.html.append("</a>");
}
@Override
public RaceLogTrackingInvitationMailBuilder addSailInsightDeeplink(String url, String iosAppUrl,
String androidAppUrl) {
this.addQrCodeImage(url) //
.addOpenInAppTextAndLinks(url, iosAppUrl, androidAppUrl) //
.addInstallAppTextAndLinks(iosAppUrl, androidAppUrl);
return this;
}
@Override
public RaceLogTrackingInvitationMailBuilder addBuoyPingerDeeplink(String url, String legacyIOSAppUrl,
String legacyAndroidAppUrl) {
this.addQrCodeImage(url) //
.addOpenInAppTextAndLinks(url, legacyIOSAppUrl, legacyAndroidAppUrl) //
.addInstallAppTextAndLinks(legacyIOSAppUrl, legacyAndroidAppUrl);
return this;
}
}
@@ -55,6 +55,7 @@ import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.RegattaLeaderboard;
import com.sap.sailing.domain.racelogtracking.PingDeviceIdentifierImpl;
import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter;
import com.sap.sailing.domain.tracking.MailInvitationType;
import com.sap.sailing.domain.tracking.RaceHandle;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.server.interfaces.RacingEventService;
@@ -286,8 +287,8 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter {
@Override
public void inviteCompetitorsForTrackingViaEmail(Event event, Leaderboard leaderboard,
String serverUrlWithoutTrailingSlash, Set<Competitor> competitors, String iOSAppUrl, String androidAppUrl,
Locale locale) throws MailException {
String serverUrlWithoutTrailingSlash, Set<Competitor> competitors, String legacyIOSAppUrl, String legacyAndroidAppUrl,
Locale locale, MailInvitationType type) throws MailException {
final StringBuilder occuredExceptions = new StringBuilder();
for (final Competitor competitor : competitors) {
final String toAddress = competitor.getEmail();
@@ -297,14 +298,12 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter {
serverUrlWithoutTrailingSlash, event.getId().toString(), leaderboard.getName(),
DeviceMappingConstants.URL_COMPETITOR_ID_AS_STRING, competitor.getId().toString(),
NonGwtUrlHelper.INSTANCE);
final RaceLogTrackingInvitationMailBuilder mail = new RaceLogTrackingInvitationMailBuilder(locale)
.withSubject(competitor.getName()) //
.addEventLogo(event) //
.addHeadline(event, leaderboard) //
.addSailInSightIntroductoryText(competitor.getName()) //
.addQrCodeImage(url) //
.addOpenInAppTextAndLinks(url, iOSAppUrl, androidAppUrl) //
.addInstallAppTextAndLinks(iOSAppUrl, androidAppUrl);
RaceLogTrackingInvitationMailBuilder mail = getMailBuilder(type, locale);
mail.withSubject(competitor.getName())
.addEventLogo(event)
.addHeadline(event, leaderboard)
.addSailInSightIntroductoryText(competitor.getName())
.addSailInsightDeeplink(url, legacyIOSAppUrl, legacyAndroidAppUrl);
getMailService().sendMail(toAddress, mail.getSubject(), mail.getMultipartSupplier());
} catch (MessagingException | MailException | IOException e) {
logger.log(Level.SEVERE, "Error while trying to send invitation mail to competitor"
@@ -320,7 +319,8 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter {
@Override
public void inviteBuoyTenderViaEmail(Event event, Leaderboard leaderboard, String serverUrlWithoutTrailingSlash,
String emails, String iOSAppUrl, String androidAppUrl, Locale locale) throws MailException {
String emails, String legacyIOSAppUrl, String legacyAndroidAppUrl, Locale locale, MailInvitationType type)
throws MailException {
final StringBuilder occuredExceptions = new StringBuilder();
final String[] emailArray = emails.split(",");
// http://<host>/buoy-tender/checkin?event_id=<event-id>&leaderboard_name=<leaderboard-name>
@@ -329,14 +329,12 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter {
for (String toAddress : emailArray) {
try {
final String buoyTender = RaceLogTrackingI18n.buoyTender(locale);
final RaceLogTrackingInvitationMailBuilder mail = new RaceLogTrackingInvitationMailBuilder(locale)
.withSubject(buoyTender) //
.addEventLogo(event) //
.addHeadline(event, leaderboard) //
.addBuoyPingerIntroductoryText(buoyTender) //
.addQrCodeImage(url) //
.addOpenInAppTextAndLinks(url, iOSAppUrl, androidAppUrl) //
.addInstallAppTextAndLinks(iOSAppUrl, androidAppUrl);
RaceLogTrackingInvitationMailBuilder mail = getMailBuilder(type, locale);
mail.withSubject(buoyTender)
.addEventLogo(event)
.addHeadline(event, leaderboard)
.addBuoyPingerIntroductoryText(buoyTender)
.addBuoyPingerDeeplink(url, legacyIOSAppUrl, legacyAndroidAppUrl);
getMailService().sendMail(toAddress, mail.getSubject(), mail.getMultipartSupplier());
} catch (MessagingException | MailException | IOException e) {
logger.log(Level.SEVERE, "Error while trying to send invitation mail to buoy tender "
@@ -348,4 +346,22 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter {
throw new MailException(occuredExceptions.toString());
}
}
private RaceLogTrackingInvitationMailBuilder getMailBuilder(MailInvitationType type, Locale locale) {
final RaceLogTrackingInvitationMailBuilder mail;
switch (type) {
case LEGACY:
mail = new LegacyRaceLogTrackingInvitationMailBuilder(locale);
break;
case SailInsight1:
mail = new BranchIO1RaceLogTrackingInvitationMailBuilder(locale);
break;
case SailInsight2:
mail = new BranchIO2RaceLogTrackingInvitationMailBuilder(locale);
break;
default:
throw new IllegalArgumentException("Unhandled mail type");
}
return mail;
}
}
@@ -16,14 +16,31 @@ public class RaceLogTrackingI18n {
return STRING_MESSAGES.get(locale, "trackingInvitationFor") + " " + invitee;
}
public static String scanQRCodeOrVisitUrlToRegisterAs(final Locale locale, final String appName) {
return STRING_MESSAGES.get(locale, "scanQRCodeOrVisitUrlToRegisterAs", appName);
public static String followBranchDeeplink(final Locale locale, final String appName, final String invitee) {
return STRING_MESSAGES.get(locale, "followBranchDeeplink", appName, invitee);
}
public static String register(final Locale locale) {
return STRING_MESSAGES.get(locale, "register");
}
public static String welcomeTo(final Locale locale, final String eventName, final String leaderboardName) {
return STRING_MESSAGES.get(locale, "welcomeTo") + " " + eventName + ", " + leaderboardName;
}
public static String buoyTender(final Locale locale) {
return STRING_MESSAGES.get(locale, "buoyTender");
}
public static String buoyPingerAppName(final Locale locale) {
return STRING_MESSAGES.get(locale, "buoyPingerAppName");
}
public static String sailInSightAppName(final Locale locale) {
return STRING_MESSAGES.get(locale, "sailInSightAppName");
}
// TODO: DELETE following once legacy is removed
public static String iOSUsers(final Locale locale) {
return STRING_MESSAGES.get(locale, "iOSUsers");
}
@@ -36,14 +53,6 @@ public class RaceLogTrackingI18n {
return STRING_MESSAGES.get(locale, "alternativelyVisitThisLink");
}
public static String buoyTender(final Locale locale) {
return STRING_MESSAGES.get(locale, "buoyTender");
}
public static String appStoreInstallText(final Locale locale) {
return STRING_MESSAGES.get(locale, "appStoreInstallText");
}
public static String appIos(final Locale locale) {
return STRING_MESSAGES.get(locale, "appIos");
}
@@ -52,12 +61,11 @@ public class RaceLogTrackingI18n {
return STRING_MESSAGES.get(locale, "appAndroid");
}
public static String buoyPingerAppName(final Locale locale) {
return STRING_MESSAGES.get(locale, "buoyPingerAppName");
public static String scanQRCodeOrVisitUrlToRegisterAs(final Locale locale, final String appName) {
return STRING_MESSAGES.get(locale, "scanQRCodeOrVisitUrlToRegisterAs", appName);
}
public static String sailInSightAppName(final Locale locale) {
return STRING_MESSAGES.get(locale, "sailInSightAppName");
public static String appStoreInstallText(final Locale locale) {
return STRING_MESSAGES.get(locale, "appStoreInstallText");
}
}
@@ -2,7 +2,6 @@ package com.sap.sailing.domain.racelogtracking.impl;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
@@ -24,48 +23,71 @@ import com.sap.sse.mail.SerializableDefaultMimeBodyPartSupplier;
import com.sap.sse.mail.SerializableFileMimeBodyPartSupplier;
import com.sap.sse.mail.SerializableMultipartMimeBodyPartSupplier;
import com.sap.sse.mail.SerializableMultipartSupplier;
import com.sap.sse.qrcode.QRCodeGenerationUtil;
import com.sap.sse.shared.media.ImageDescriptor;
/**
* Builder to create invitation mails for competitor and buoy tracking. This class ensures that the mail is being sent
* as text and html body to support a great variety of mail clients.
*/
class RaceLogTrackingInvitationMailBuilder {
abstract class RaceLogTrackingInvitationMailBuilder {
private static final Logger LOG = Logger.getLogger(RaceLogTrackingInvitationMailBuilder.class.getName());
/**
* System separator can't be used, as the reader might use any mail client and OS. This line break seems to work reliable over all
* systems.
* System separator can't be used, as the reader might use any mail client and OS. This line break seems to work
* reliable over all systems.
*/
private static final String TEXT_LINE_BREAK = "\r\n";
/**
* URL prefix, so the iOS app will recognize as deep link and pass anything after it to the app for analysis.
*/
private static final String IOS_DEEP_LINK_PREFIX = "comsapsailingtracker://";
private final Locale locale;
private final Map<String,byte[]> pngAttachAndInline = new HashMap<>();
private final StringBuilder html = new StringBuilder();
private final StringBuilder text = new StringBuilder();
protected static final String TEXT_LINE_BREAK = "\r\n";
private String subject;
protected final Locale locale;
/**
* @param locale
* the locale in which the resulting mail will be generated. If there are no specific messages available
* for the given locale, English locale is used as fallback.
*/
RaceLogTrackingInvitationMailBuilder(final Locale locale) {
protected final Map<String, byte[]> pngAttachAndInline = new HashMap<>();
protected final StringBuilder html = new StringBuilder();
protected final StringBuilder text = new StringBuilder();
public RaceLogTrackingInvitationMailBuilder(Locale locale) {
this.locale = locale;
}
abstract protected void addIntroductoryText(String appName, String invitee);
abstract public RaceLogTrackingInvitationMailBuilder addSailInsightDeeplink(String url, String legacyIOSAppUrl,
String legacyAndroidAppUrl);
abstract public RaceLogTrackingInvitationMailBuilder addBuoyPingerDeeplink(String url, String legacyIOSAppUrl,
String legacyAndroidAppUrl);
RaceLogTrackingInvitationMailBuilder withSubject(final String invitee) {
this.subject = RaceLogTrackingI18n.trackingInvitationFor(locale, invitee);
return this;
}
protected void insertImage(byte[] cidImage, String cidSource, String alt) {
this.pngAttachAndInline.put(cidSource, cidImage);
this.html.append("<img alt=\"").append(alt).append("\" src=\"cid:").append(cidSource).append("\"/>");
this.html.append("<br>");
}
public String getSubject() {
return subject;
}
RaceLogTrackingInvitationMailBuilder addEventLogo(final Event event) {
final List<ImageDescriptor> imagesWithTag = event.findImagesWithTag(MediaTagConstants.LOGO.getName());
if (imagesWithTag != null && !imagesWithTag.isEmpty()) {
final ImageDescriptor imageDescriptor = imagesWithTag.get(0);
final String logoUrl = imageDescriptor.getURL().toString();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream is = new URL(logoUrl).openStream();) {
BufferedImage image = ImageIO.read(is);
ImageIO.write(image, "png", baos);
insertImage(baos.toByteArray(), "logo", logoUrl);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error while getting event image for invitation mail", e);
}
}
return this;
}
RaceLogTrackingInvitationMailBuilder addHeadline(final Event event, final Leaderboard leaderboard) {
final String lbdn = leaderboard.getDisplayName() == null ? leaderboard.getName() : leaderboard.getDisplayName();
final String welcomeText = RaceLogTrackingI18n.welcomeTo(locale, event.getName(), lbdn);
@@ -84,90 +106,6 @@ class RaceLogTrackingInvitationMailBuilder {
return this;
}
RaceLogTrackingInvitationMailBuilder addEventLogo(final Event event) {
final List<ImageDescriptor> imagesWithTag = event.findImagesWithTag(MediaTagConstants.LOGO.getName());
if (imagesWithTag != null && !imagesWithTag.isEmpty()) {
final ImageDescriptor imageDescriptor = imagesWithTag.get(0);
final String logoUrl = imageDescriptor.getURL().toString();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream is = new URL(logoUrl).openStream();) {
BufferedImage image = ImageIO.read(is);
ImageIO.write(image, "png", baos);
insertImage(baos.toByteArray(), "logo", logoUrl);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error while getting event image for invitation mail", e);
}
}
return this;
}
RaceLogTrackingInvitationMailBuilder addQrCodeImage(final String url) {
try (DataInputStream imageIs = new DataInputStream(QRCodeGenerationUtil.create(url, 250))) {
byte[] targetArray = new byte[imageIs.available()];
imageIs.readFully(targetArray);
insertImage(targetArray, "qr", url);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error while generating QR code for invitation mail", e);
}
return this;
}
RaceLogTrackingInvitationMailBuilder addOpenInAppTextAndLinks(final String targetUrl, final String iOSAppUrl,
final String androidAppUrl) {
final boolean hasIOSAppUrl = iOSAppUrl != null && !iOSAppUrl.isEmpty();
final boolean hasAndroidAppUrl = androidAppUrl != null && !androidAppUrl.isEmpty();
if (hasIOSAppUrl || hasAndroidAppUrl) {
this.html.append("<p>").append(RaceLogTrackingI18n.alternativelyVisitThisLink(this.locale)).append("</p>");
}
this.html.append("<table border=\"0\" cellspacing=\"20px\" cellpadding=\"0px\">");
this.html.append("<tr>");
if (hasIOSAppUrl) {
this.html.append("<td>");
this.addHtmlLink(IOS_DEEP_LINK_PREFIX + targetUrl, RaceLogTrackingI18n::iOSUsers);
this.html.append("</td>");
this.addTextLink(IOS_DEEP_LINK_PREFIX + targetUrl, RaceLogTrackingI18n::iOSUsers);
}
if (hasAndroidAppUrl) {
this.html.append("<td>");
this.addHtmlLink(targetUrl, RaceLogTrackingI18n::androidUsers);
this.html.append("</td>");
this.addTextLink(targetUrl, RaceLogTrackingI18n::androidUsers);
}
this.html.append("</tr>");
this.html.append("</table>");
return this;
}
RaceLogTrackingInvitationMailBuilder addInstallAppTextAndLinks(final String iOSAppUrl, final String androidAppUrl) {
final boolean hasIOSAppUrl = iOSAppUrl != null && !iOSAppUrl.isEmpty();
final boolean hasAndroidAppUrl = androidAppUrl != null && !androidAppUrl.isEmpty();
if (hasIOSAppUrl || hasAndroidAppUrl) {
this.html.append("<p>").append(RaceLogTrackingI18n.appStoreInstallText(this.locale)).append("</p>");
text.append(RaceLogTrackingI18n.appStoreInstallText(locale));
text.append(TEXT_LINE_BREAK);
}
this.html.append("<table border=\"0\" cellspacing=\"20px\" cellpadding=\"0px\">");
this.html.append("<tr>");
if (hasIOSAppUrl) {
this.html.append("<td>");
this.addHtmlLink(iOSAppUrl, RaceLogTrackingI18n::appIos);
this.html.append("</td>");
this.addTextLink(iOSAppUrl, RaceLogTrackingI18n::appIos);
}
if (hasAndroidAppUrl) {
this.html.append("<td>");
this.addHtmlLink(androidAppUrl, RaceLogTrackingI18n::appAndroid);
this.html.append("</td>");
this.addTextLink(androidAppUrl, RaceLogTrackingI18n::appAndroid);
}
this.html.append("</tr>");
this.html.append("</table>");
return this;
}
String getSubject() {
return subject;
}
SerializableMultipartSupplier getMultipartSupplier() throws MessagingException {
final SerializableMultipartSupplier mixedSupplier = new SerializableMultipartSupplier("mixed");
@@ -176,12 +114,12 @@ class RaceLogTrackingInvitationMailBuilder {
mixedSupplier.addBodyPart(new SerializableMultipartMimeBodyPartSupplier(alternativeSupplier));
alternativeSupplier.addBodyPart(new SerializableDefaultMimeBodyPartSupplier(text.toString(), "text/plain"));
final SerializableMultipartSupplier relatedSupplier = new SerializableMultipartSupplier("related");
alternativeSupplier.addBodyPart(new SerializableMultipartMimeBodyPartSupplier(relatedSupplier));
relatedSupplier.addBodyPart(new SerializableDefaultMimeBodyPartSupplier(html.toString(), "text/html"));
for (Entry<String, byte[]> imageEntry : pngAttachAndInline.entrySet()) {
final String contentType = "image/png";
final String cid = "<" + imageEntry.getKey() + ">";
@@ -190,43 +128,15 @@ class RaceLogTrackingInvitationMailBuilder {
relatedSupplier.addBodyPart(new SerializableFileMimeBodyPartSupplier(img, contentType, cid, filename));
mixedSupplier.addBodyPart(new SerializableFileMimeBodyPartSupplier(img, contentType, filename));
}
return mixedSupplier;
}
private void addIntroductoryText(final String appName, final String invitee) {
final String introText = RaceLogTrackingI18n.scanQRCodeOrVisitUrlToRegisterAs(locale, appName);
this.html.append("<p>").append(introText).append(" <b>").append(invitee).append("</b></p>");
this.text.append(introText).append(" ").append(invitee).append(TEXT_LINE_BREAK).append(TEXT_LINE_BREAK);
}
private void insertImage(byte[] cidImage, String cidSource, String alt) {
this.pngAttachAndInline.put(cidSource, cidImage);
this.html.append("<img alt=\"").append(alt).append("\" src=\"cid:").append(cidSource).append("\"/>");
this.html.append("<br>");
}
private void addTextLink(String url, final Function<Locale, String> textFactory) {
protected void addTextLink(String url, final Function<Locale, String> textFactory) {
text.append(textFactory.apply(locale)).append(":");
text.append(TEXT_LINE_BREAK);
text.append(url);
text.append(TEXT_LINE_BREAK);
text.append(TEXT_LINE_BREAK);
}
private void addHtmlLink(final String url, final Function<Locale, String> textFactory) {
final String anchor = "<a href=\"" + url + "\" style=\"color:#fff; text-decoration:none;\">";
// outer href for clickablity of the whole button
this.html.append(anchor);
// table being used for the styling, as some mail clients have isssues with divs
this.html.append("<table width=\"100%\" style=\"background-color: #337ab7; color:#fff;\"><tr>");
this.html.append("<td style=\"padding: 15px; text-align: center; color:#fff;\">");
// inner href, so that outlook does work (it does not recognize the outer)
this.html.append(anchor);
this.html.append(textFactory.apply(locale));
this.html.append("</a>");
this.html.append("</td>");
this.html.append("</tr></table>");
this.html.append("</a>");
}
}
@@ -0,0 +1,7 @@
package com.sap.sailing.domain.tracking;
public enum MailInvitationType {
LEGACY,
SailInsight1,
SailInsight2
}
@@ -33,6 +33,7 @@ public class QRCodePlace extends AbstractBasePlace {
}
public enum InvitationMode {
COMPETITOR_2,
COMPETITOR,
BOUY_TENDER
}
@@ -49,7 +50,8 @@ public class QRCodePlace extends AbstractBasePlace {
if (leaderboardName == null) {
GWT.log("No parameter " + PARAM_LEADERBOARD_NAME + " found!");
}
if (competitorId == null && mode == InvitationMode.COMPETITOR) {
if (competitorId == null
&& (mode == InvitationMode.COMPETITOR || mode == InvitationMode.COMPETITOR_2)) {
GWT.log("No parameter " + PARAM_COMPETITOR_ID + " found!");
}
if (competitorId != null && mode == InvitationMode.BOUY_TENDER) {
@@ -30,7 +30,9 @@ public class QRCodePresenter {
}
public void setView(QRCodeView view) {
if (eventId == null || invitationMode == InvitationMode.COMPETITOR && competitorId == null
if (eventId == null
|| (invitationMode == InvitationMode.COMPETITOR || invitationMode == InvitationMode.COMPETITOR_2)
&& competitorId == null
|| leaderboardNameFromUrl == null
|| leaderboardNameFromUrl.equals("") || checkInUrl == null || checkInUrl.equals("")) {
view.setError();
@@ -118,6 +120,11 @@ public class QRCodePresenter {
+ BranchIOConstants.SAILINSIGHT_APP_BRANCHIO_PATH + "="
+ QRCodePresenter.this.checkInUrl;
break;
case COMPETITOR_2:
branchIoUrl = BranchIOConstants.SAILINSIGHT_2_APP_BRANCHIO + "?"
+ BranchIOConstants.SAILINSIGHT_APP_BRANCHIO_PATH + "="
+ QRCodePresenter.this.checkInUrl;
break;
default:
break;
}
@@ -54,6 +54,7 @@ public class QRCodeView extends Composite {
InvitationMode invitationMode) {
switch (invitationMode) {
case COMPETITOR:
case COMPETITOR_2:
titleDivUi.setInnerText(StringMessages.INSTANCE.qrCodeTitle(competitor.getName(), leaderboardName));
subtitleDivUi.setInnerText(
StringMessages.INSTANCE.qrCodeSubtitle(event.getDisplayName(), event.getLocationAndVenue()));
@@ -351,6 +351,7 @@ import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
import com.sap.sailing.domain.tracking.GPSFixTrack;
import com.sap.sailing.domain.tracking.LineDetails;
import com.sap.sailing.domain.tracking.MailInvitationType;
import com.sap.sailing.domain.tracking.Maneuver;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RaceHandle;
@@ -652,6 +653,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
private static final int LEADERBOARD_DIFFERENCE_CACHE_SIZE = 50;
private static final String MAILTYPE_PROPERTY = "com.sap.sailing.domain.tracking.MailInvitationType";
private ResourceBundleStringMessages serverStringMessages;
private final LinkedHashMap<String, LeaderboardDTO> leaderboardByNameResultsCacheById;
@@ -6714,9 +6717,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
for (CompetitorDTO c : competitorDtos) {
competitors.add(getCompetitor(c));
}
MailInvitationType type = MailInvitationType
.valueOf(System.getProperty(MAILTYPE_PROPERTY, MailInvitationType.LEGACY.name()));
Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName);
getRaceLogTrackingAdapter().inviteCompetitorsForTrackingViaEmail(event, leaderboard, serverUrlWithoutTrailingSlash,
competitors, iOSAppUrl, androidAppUrl, getLocale(localeInfoName));
competitors, iOSAppUrl, androidAppUrl, getLocale(localeInfoName), type);
}
@Override
@@ -6725,8 +6730,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
throws MailException {
Event event = getService().getEvent(eventDto.id);
Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName);
getRaceLogTrackingAdapter().inviteBuoyTenderViaEmail(event, leaderboard, serverUrlWithoutTrailingSlash,
emails, iOSAppUrl, androidAppUrl, getLocale(localeInfoName));
MailInvitationType type = MailInvitationType
.valueOf(System.getProperty(MAILTYPE_PROPERTY, MailInvitationType.LEGACY.name()));
getRaceLogTrackingAdapter().inviteBuoyTenderViaEmail(event, leaderboard, serverUrlWithoutTrailingSlash, emails,
iOSAppUrl, androidAppUrl, getLocale(localeInfoName), type);
}
@Override