discoveryResults = inbox.stream()
+ .filter(discoveryResult -> willConnectVia(discoveryResult, bridge)).collect(Collectors.toList());
+
+ return templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings, discoveryResults);
+ }
+
+ private boolean isConnectedVia(Thing thing, Bridge bridge) {
+ return bridge.getUID().equals(thing.getBridgeUID());
+ }
+
+ private boolean willConnectVia(DiscoveryResult discoveryResult, Bridge bridge) {
+ return bridge.getUID().equals(discoveryResult.getBridgeUID());
+ }
+
+ private boolean isMieleCloudBridge(Thing thing) {
+ return MieleCloudBindingConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID());
+ }
+
+ private String renderSslWarning(HttpServletRequest request, String skeleton) {
+ if (!request.isSecure()) {
+ return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "\n"
+ + " Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange.\n"
+ + " See
Securing access to openHAB for details.\n"
+ + "
");
+ } else {
+ return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java
new file mode 100644
index 0000000000000..3b667ce183da1
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java
@@ -0,0 +1,217 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.config.exception.BridgeCreationFailedException;
+import org.openhab.binding.mielecloud.internal.config.exception.BridgeReconfigurationFailedException;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet that automatically creates a bridge and then redirects the browser to the account overview page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class CreateBridgeServlet extends AbstractRedirectionServlet {
+ private static final String MIELE_CLOUD_BRIDGE_NAME = "Cloud Connector";
+ private static final String MIELE_CLOUD_BRIDGE_LABEL = "Miele@home Account";
+
+ private static final String LOCALE_PARAMETER_NAME = "locale";
+ public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
+ public static final String EMAIL_PARAMETER_NAME = "email";
+
+ private static final long serialVersionUID = -2912042079128722887L;
+
+ private static final String DEFAULT_LOCALE = "en";
+
+ private static final long ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS = 5000;
+ private static final long DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS = 5000;
+ private static final long CHECK_INTERVAL_IN_MILLISECONDS = 100;
+
+ private final Logger logger = LoggerFactory.getLogger(CreateBridgeServlet.class);
+
+ private final Inbox inbox;
+ private final ThingRegistry thingRegistry;
+
+ /**
+ * Creates a new {@link CreateBridgeServlet}.
+ *
+ * @param inbox openHAB inbox for discovery results.
+ * @param thingRegistry openHAB thing registry.
+ */
+ public CreateBridgeServlet(Inbox inbox, ThingRegistry thingRegistry) {
+ this.inbox = inbox;
+ this.thingRegistry = thingRegistry;
+ }
+
+ @Override
+ protected String getRedirectionDestination(HttpServletRequest request) {
+ String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
+ if (bridgeUidString == null || bridgeUidString.isEmpty()) {
+ logger.warn("Cannot create bridge: Bridge UID is missing.");
+ return "/mielecloud/failure?" + FailureServlet.MISSING_BRIDGE_UID_PARAMETER_NAME + "=true";
+ }
+
+ String email = request.getParameter(EMAIL_PARAMETER_NAME);
+ if (email == null || email.isEmpty()) {
+ logger.warn("Cannot create bridge: E-mail address is missing.");
+ return "/mielecloud/failure?" + FailureServlet.MISSING_EMAIL_PARAMETER_NAME + "=true";
+ }
+
+ ThingUID bridgeUid = null;
+ try {
+ bridgeUid = new ThingUID(bridgeUidString);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Cannot create bridge: Bridge UID '{}' is malformed.", bridgeUid);
+ return "/mielecloud/failure?" + FailureServlet.MALFORMED_BRIDGE_UID_PARAMETER_NAME + "=true";
+ }
+
+ if (!EmailValidator.isValid(email)) {
+ logger.warn("Cannot create bridge: E-mail address '{}' is malformed.", email);
+ return "/mielecloud/failure?" + FailureServlet.MALFORMED_EMAIL_PARAMETER_NAME + "=true";
+ }
+
+ String locale = getValidLocale(request.getParameter(LOCALE_PARAMETER_NAME));
+
+ logger.debug("Auto configuring Miele account using locale '{}' (requested locale was '{}')", locale,
+ request.getParameter(LOCALE_PARAMETER_NAME));
+ try {
+ Thing bridge = pairOrReconfigureBridge(locale, bridgeUid, email);
+ waitForBridgeToComeOnline(bridge);
+ return "/mielecloud";
+ } catch (BridgeReconfigurationFailedException e) {
+ logger.warn("{}", e.getMessage());
+ return "/mielecloud/success?" + SuccessServlet.BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME + "=true&"
+ + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+ } catch (BridgeCreationFailedException e) {
+ logger.warn("Thing creation failed because there was no binding available that supports the thing.");
+ return "/mielecloud/success?" + SuccessServlet.BRIDGE_CREATION_FAILED_PARAMETER_NAME + "=true&"
+ + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+ }
+ }
+
+ private Thing pairOrReconfigureBridge(String locale, ThingUID bridgeUid, String email) {
+ DiscoveryResult result = DiscoveryResultBuilder.create(bridgeUid)
+ .withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withLabel(MIELE_CLOUD_BRIDGE_LABEL)
+ .withProperty(Thing.PROPERTY_MODEL_ID, MIELE_CLOUD_BRIDGE_NAME)
+ .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE, locale)
+ .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, email).build();
+ if (inbox.add(result)) {
+ return pairBridge(bridgeUid);
+ } else {
+ return reconfigureBridge(bridgeUid, locale, email);
+ }
+ }
+
+ private Thing pairBridge(ThingUID thingUid) {
+ Thing thing = inbox.approve(thingUid, MIELE_CLOUD_BRIDGE_LABEL, null);
+ if (thing == null) {
+ throw new BridgeCreationFailedException();
+ }
+
+ logger.debug("Successfully created bridge {}", thingUid);
+ return thing;
+ }
+
+ private Thing reconfigureBridge(ThingUID thingUid, String locale, String email) {
+ logger.debug("Thing already exists. Modifying configuration.");
+ Thing thing = thingRegistry.get(thingUid);
+ if (thing == null) {
+ throw new BridgeReconfigurationFailedException(
+ "Cannot modify non existing bridge: Could neither add bridge via inbox nor find existing bridge.");
+ }
+
+ ThingHandler handler = thing.getHandler();
+ if (handler == null) {
+ throw new BridgeReconfigurationFailedException("Bridge exists but has no handler.");
+ }
+ if (!(handler instanceof MieleBridgeHandler)) {
+ throw new BridgeReconfigurationFailedException("Bridge handler is of wrong type, expected '"
+ + MieleBridgeHandler.class.getSimpleName() + "' but got '" + handler.getClass().getName() + "'.");
+ }
+
+ MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) handler;
+ bridgeHandler.disposeWebservice();
+ bridgeHandler.initializeWebservice();
+
+ return thing;
+ }
+
+ private String getValidLocale(@Nullable String localeParameterValue) {
+ if (localeParameterValue == null || localeParameterValue.isEmpty()
+ || !LocaleValidator.isValidLanguage(localeParameterValue)) {
+ return DEFAULT_LOCALE;
+ } else {
+ return localeParameterValue;
+ }
+ }
+
+ private void waitForBridgeToComeOnline(Thing bridge) {
+ try {
+ waitForConditionWithTimeout(() -> bridge.getStatus() == ThingStatus.ONLINE,
+ ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS);
+ waitForConditionWithTimeout(new DiscoveryResultCountDoesNotChangeCondition(),
+ DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void waitForConditionWithTimeout(BooleanSupplier condition, long timeoutInMilliseconds)
+ throws InterruptedException {
+ long remainingWaitTime = timeoutInMilliseconds;
+ while (!condition.getAsBoolean() && remainingWaitTime > 0) {
+ TimeUnit.MILLISECONDS.sleep(CHECK_INTERVAL_IN_MILLISECONDS);
+ remainingWaitTime -= CHECK_INTERVAL_IN_MILLISECONDS;
+ }
+ }
+
+ private class DiscoveryResultCountDoesNotChangeCondition implements BooleanSupplier {
+ private long previousDiscoveryResultCount = 0;
+
+ @Override
+ public boolean getAsBoolean() {
+ var discoveryResultCount = countOwnDiscoveryResults();
+ var discoveryResultCountUnchanged = previousDiscoveryResultCount == discoveryResultCount;
+ previousDiscoveryResultCount = discoveryResultCount;
+ return discoveryResultCountUnchanged;
+ }
+
+ private long countOwnDiscoveryResults() {
+ return inbox.stream().map(DiscoveryResult::getBindingId)
+ .filter(MieleCloudBindingConstants.BINDING_ID::equals).count();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java
new file mode 100644
index 0000000000000..a24802b3b298f
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Servlet showing a failure page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class FailureServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = -5195984256535664942L;
+
+ public static final String OAUTH2_ERROR_PARAMETER_NAME = "oauth2Error";
+ public static final String ILLEGAL_RESPONSE_PARAMETER_NAME = "illegalResponse";
+ public static final String NO_ONGOING_AUTHORIZATION_PARAMETER_NAME = "noOngoingAuthorization";
+ public static final String FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME = "failedToCompleteAuthorization";
+ public static final String MISSING_BRIDGE_UID_PARAMETER_NAME = "missingBridgeUid";
+ public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
+ public static final String MALFORMED_BRIDGE_UID_PARAMETER_NAME = "malformedBridgeUid";
+ public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
+ public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
+
+ public static final String OAUTH2_ERROR_ACCESS_DENIED = "access_denied";
+ public static final String OAUTH2_ERROR_INVALID_REQUEST = "invalid_request";
+ public static final String OAUTH2_ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client";
+ public static final String OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
+ public static final String OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope";
+ public static final String OAUTH2_ERROR_SERVER_ERROR = "server_error";
+ public static final String OAUTH2_ERROR_TEMPORARY_UNAVAILABLE = "temporarily_unavailable";
+
+ private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "";
+
+ /**
+ * Creates a new {@link FailureServlet}.
+ *
+ * @param resourceLoader Loader to use for resources.
+ */
+ public FailureServlet(ResourceLoader resourceLoader) {
+ super(resourceLoader);
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ getErrorMessage(request));
+ }
+
+ private String getErrorMessage(HttpServletRequest request) {
+ String oauth2Error = request.getParameter(OAUTH2_ERROR_PARAMETER_NAME);
+ if (oauth2Error != null) {
+ return getOAuth2ErrorMessage(oauth2Error);
+ } else if (ServletUtil.isParameterEnabled(request, ILLEGAL_RESPONSE_PARAMETER_NAME)) {
+ return "Miele cloud service returned an illegal response.";
+ } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_PARAMETER_NAME)) {
+ return "There is no ongoing authorization. Please start an authorization first.";
+ } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME)) {
+ return "Completing the final authorization request failed. Please try the config flow again.";
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_UID_PARAMETER_NAME)) {
+ return "Missing bridge UID.";
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
+ return "Missing e-mail address.";
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_UID_PARAMETER_NAME)) {
+ return "Malformed bridge UID.";
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
+ return "Malformed e-mail address.";
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
+ return "Missing request URL. Please try the config flow again.";
+ } else {
+ return "Unknown error.";
+ }
+ }
+
+ private String getOAuth2ErrorMessage(String oauth2Error) {
+ return "OAuth2 authentication with Miele cloud service failed: " + getOAuth2ErrorDetailMessage(oauth2Error);
+ }
+
+ private String getOAuth2ErrorDetailMessage(String oauth2Error) {
+ switch (oauth2Error) {
+ case OAUTH2_ERROR_ACCESS_DENIED:
+ return "Access denied.";
+ case OAUTH2_ERROR_INVALID_REQUEST:
+ return "Malformed request.";
+ case OAUTH2_ERROR_UNAUTHORIZED_CLIENT:
+ return "Account not authorized to request authorization code.";
+ case OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE:
+ return "Obtaining an authorization code is not supported.";
+ case OAUTH2_ERROR_INVALID_SCOPE:
+ return "Invalid scope.";
+ case OAUTH2_ERROR_SERVER_ERROR:
+ return "Unexpected server error.";
+ case OAUTH2_ERROR_TEMPORARY_UNAVAILABLE:
+ return "Authorization server temporarily unavailable.";
+ default:
+ return "Unknown error code \"" + oauth2Error + "\".";
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java
new file mode 100644
index 0000000000000..e817463adf87d
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet gathers and processes required information to perform an authorization with the Miele cloud service
+ * and create a bridge afterwards. Required parameters are the client ID, client secret, an ID for the bridge and an
+ * e-mail address. If the given parameters are valid, the browser is redirected to the Miele service login. Otherwise,
+ * the browser is redirected to the previous page with an according error message.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ForwardToLoginServlet extends AbstractRedirectionServlet {
+ private static final long serialVersionUID = -9094642228439994183L;
+
+ public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
+ public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
+ public static final String BRIDGE_ID_PARAMETER_NAME = "bridgeId";
+ public static final String EMAIL_PARAMETER_NAME = "email";
+
+ private final Logger logger = LoggerFactory.getLogger(ForwardToLoginServlet.class);
+
+ private final OAuthAuthorizationHandler authorizationHandler;
+
+ /**
+ * Creates a new {@link ForwardToLoginServlet}.
+ *
+ * @param authorizationHandler Handler implementing the OAuth authorization process.
+ */
+ public ForwardToLoginServlet(OAuthAuthorizationHandler authorizationHandler) {
+ this.authorizationHandler = authorizationHandler;
+ }
+
+ @Override
+ protected String getRedirectionDestination(HttpServletRequest request) {
+ String clientId = request.getParameter(CLIENT_ID_PARAMETER_NAME);
+ String clientSecret = request.getParameter(CLIENT_SECRET_PARAMETER_NAME);
+ String bridgeId = request.getParameter(BRIDGE_ID_PARAMETER_NAME);
+ String email = request.getParameter(EMAIL_PARAMETER_NAME);
+
+ if (clientId == null || clientId.isEmpty()) {
+ logger.warn("Request is missing client ID.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_ID_PARAMETER_NAME);
+ }
+ if (clientSecret == null || clientSecret.isEmpty()) {
+ logger.warn("Request is missing client secret.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_SECRET_PARAMETER_NAME);
+ }
+ if (bridgeId == null || bridgeId.isEmpty()) {
+ logger.warn("Request is missing bridge ID.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_BRIDGE_ID_PARAMETER_NAME);
+ }
+ if (email == null || email.isEmpty()) {
+ logger.warn("Request is missing e-mail address.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_EMAIL_PARAMETER_NAME);
+ }
+
+ ThingUID bridgeUid = null;
+ try {
+ bridgeUid = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, bridgeId);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Passed bridge ID '{}' is invalid.", bridgeId);
+ return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_BRIDGE_ID_PARAMETER_NAME);
+ }
+
+ if (!EmailValidator.isValid(email)) {
+ logger.warn("Passed e-mail address '{}' is invalid.", email);
+ return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_EMAIL_PARAMETER_NAME);
+ }
+
+ try {
+ authorizationHandler.beginAuthorization(clientId, clientSecret, bridgeUid, email);
+ } catch (OngoingAuthorizationException e) {
+ logger.warn("Cannot begin new authorization process while another one is still running.");
+ return getErrorRedirectUrlWithExpiryTime(e.getOngoingAuthorizationExpiryTimestamp());
+ }
+
+ StringBuffer requestUrl = request.getRequestURL();
+ if (requestUrl == null) {
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_REQUEST_URL_PARAMETER_NAME);
+ }
+
+ try {
+ return authorizationHandler.getAuthorizationUrl(deriveRedirectUri(requestUrl.toString()));
+ } catch (NoOngoingAuthorizationException e) {
+ logger.warn(
+ "Failed to create authorization URL: There was no ongoing authorization although we just started one.");
+ return getErrorRedirectionUrl(PairAccountServlet.NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME);
+ } catch (OAuthException e) {
+ logger.warn("Failed to create authorization URL.", e);
+ return getErrorRedirectionUrl(PairAccountServlet.FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME);
+ }
+ }
+
+ private String getErrorRedirectUrlWithExpiryTime(@Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
+ if (ongoingAuthorizationExpiryTimestamp == null) {
+ return getErrorRedirectionUrl(
+ PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
+ PairAccountServlet.ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME);
+ }
+
+ long minutesUntilExpiry = ChronoUnit.MINUTES.between(LocalDateTime.now(), ongoingAuthorizationExpiryTimestamp)
+ + 1;
+ return getErrorRedirectionUrl(
+ PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
+ Long.toString(minutesUntilExpiry));
+ }
+
+ private String getErrorRedirectionUrl(String errorCode) {
+ return getErrorRedirectionUrl(errorCode, "true");
+ }
+
+ private String getErrorRedirectionUrl(String errorCode, String parameterValue) {
+ return "/mielecloud/pair?" + errorCode + "=" + parameterValue;
+ }
+
+ private String deriveRedirectUri(String requestUrl) {
+ return requestUrl + "/../result";
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java
new file mode 100644
index 0000000000000..c5eff7bc6697b
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception wrapping a HTTP error code for further processing.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class MieleHttpException extends Exception {
+ private static final long serialVersionUID = 1825214275413952809L;
+
+ private final int httpErrorCode;
+
+ public MieleHttpException(int httpErrorCode) {
+ this.httpErrorCode = httpErrorCode;
+ }
+
+ public int getHttpErrorCode() {
+ return httpErrorCode;
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java
new file mode 100644
index 0000000000000..79d872a7caf4a
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Servlet showing the pair account page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class PairAccountServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = 6565378471951635420L;
+
+ public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
+ public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
+
+ public static final String MISSING_CLIENT_ID_PARAMETER_NAME = "missingClientId";
+ public static final String MISSING_CLIENT_SECRET_PARAMETER_NAME = "missingClientSecret";
+ public static final String MISSING_BRIDGE_ID_PARAMETER_NAME = "missingBridgeId";
+ public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
+ public static final String MALFORMED_BRIDGE_ID_PARAMETER_NAME = "malformedBridgeId";
+ public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
+ public static final String FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME = "failedToDeriveRedirectUrl";
+ public static final String ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME = "ongoingAuthorizationInStep1ExpiresInMinutes";
+ public static final String ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME = "unknown";
+ public static final String NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME = "noOngoingAuthorizationInStep2";
+ public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
+
+ private static final String PAIR_ACCOUNT_SKELETON = "pairing.html";
+
+ private static final String CLIENT_ID_PLACEHOLDER = "";
+ private static final String CLIENT_SECRET_PLACEHOLDER = "";
+ private static final String ERROR_MESSAGE_PLACEHOLDER = "";
+
+ /**
+ * Creates a new {@link PairAccountServlet}.
+ *
+ * @param resourceLoader Loader for resources.
+ */
+ public PairAccountServlet(ResourceLoader resourceLoader) {
+ super(resourceLoader);
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ String skeleton = getResourceLoader().loadResourceAsString(PAIR_ACCOUNT_SKELETON);
+ skeleton = renderClientIdAndClientSecret(request, skeleton);
+ skeleton = renderErrorMessage(request, skeleton);
+ return skeleton;
+ }
+
+ private String renderClientIdAndClientSecret(HttpServletRequest request, String skeleton) {
+ String prefilledClientId = Optional.ofNullable(request.getParameter(CLIENT_ID_PARAMETER_NAME)).orElse("");
+ String prefilledClientSecret = Optional.ofNullable(request.getParameter(CLIENT_SECRET_PARAMETER_NAME))
+ .orElse("");
+ return skeleton.replace(CLIENT_ID_PLACEHOLDER, prefilledClientId).replace(CLIENT_SECRET_PLACEHOLDER,
+ prefilledClientSecret);
+ }
+
+ private String renderErrorMessage(HttpServletRequest request, String skeleton) {
+ if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_ID_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Missing client ID.
");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_SECRET_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+
+ "Missing client secret.
");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_ID_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Missing bridge ID.
");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Missing e-mail address.
");
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_ID_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!
");
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Malformed e-mail address.
");
+ } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Failed to derive redirect URL.
");
+ } else if (ServletUtil.isParameterPresent(request,
+ ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME)) {
+ String minutesUntilExpiry = request
+ .getParameter(ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME);
+ if (ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME.equals(minutesUntilExpiry)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again later.
");
+ } else {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in "
+ + minutesUntilExpiry + " minutes.
");
+ }
+ } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?
");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "Missing request URL. Please try again.
");
+ } else {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, "");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java
new file mode 100644
index 0000000000000..d93a3c9999f19
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.osgi.framework.BundleContext;
+
+/**
+ * Provides access to resource files for servlets.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ResourceLoader {
+ private static final String BEGINNING_OF_INPUT = "\\A";
+
+ private final String basePath;
+ private final BundleContext bundleContext;
+
+ /**
+ * Creates a new {@link ResourceLoader}.
+ *
+ * @param basePath The base path to use for loading. A trailing {@code "/"} is removed.
+ * @param bundleContext {@link BundleContext} to load from.
+ */
+ public ResourceLoader(String basePath, BundleContext bundleContext) {
+ this.basePath = removeTrailingSlashes(basePath);
+ this.bundleContext = bundleContext;
+ }
+
+ private String removeTrailingSlashes(String value) {
+ String ret = value;
+ while (ret.endsWith("/")) {
+ ret = ret.substring(0, ret.length() - 1);
+ }
+ return ret;
+ }
+
+ /**
+ * Opens a resource relative to the base path.
+ *
+ * @param filename The filename of the resource to load.
+ * @return A stream reading from the resource file.
+ * @throws FileNotFoundException If the requested resource file cannot be found.
+ * @throws IOException If an error occurs while opening a stream to the resource.
+ */
+ public InputStream openResource(String filename) throws IOException {
+ URL url = bundleContext.getBundle().getEntry(basePath + "/" + filename);
+ if (url == null) {
+ throw new FileNotFoundException("Cannot find '" + filename + "' relative to '" + basePath + "'");
+ }
+
+ return url.openStream();
+ }
+
+ /**
+ * Loads the contents of a resource file as UTF-8 encoded {@link String}.
+ *
+ * @param filename The filename of the resource to load.
+ * @return The contents of the file.
+ * @throws FileNotFoundException If the requested resource file cannot be found.
+ * @throws IOException If an error occurs while opening a stream to the resource or reading from it.
+ */
+ public String loadResourceAsString(String filename) throws IOException {
+ try (Scanner scanner = new Scanner(openResource(filename), StandardCharsets.UTF_8.name())) {
+ return scanner.useDelimiter(BEGINNING_OF_INPUT).next();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java
new file mode 100644
index 0000000000000..5a5db09090967
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet processing the response by the Miele service after a login. This servlet is called as a result of a
+ * completed login to the Miele service and assumes that the OAuth 2 parameters are passed. Depending on the parameters
+ * and whether the token response can be fetched either the browser is redirected to the success or the failure page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ResultServlet extends AbstractRedirectionServlet {
+ private static final long serialVersionUID = 2157912755568949550L;
+
+ public static final String CODE_PARAMETER_NAME = "code";
+ public static final String STATE_PARAMETER_NAME = "state";
+ public static final String ERROR_PARAMETER_NAME = "error";
+
+ private final Logger logger = LoggerFactory.getLogger(ResultServlet.class);
+
+ private final OAuthAuthorizationHandler authorizationHandler;
+
+ /**
+ * Creates a new {@link ResultServlet}.
+ *
+ * @param authorizationHandler Handler implementing the OAuth authorization.
+ */
+ public ResultServlet(OAuthAuthorizationHandler authorizationHandler) {
+ this.authorizationHandler = authorizationHandler;
+ }
+
+ @Override
+ protected String getRedirectionDestination(HttpServletRequest request) {
+ String error = request.getParameter(ERROR_PARAMETER_NAME);
+ if (error != null) {
+ logger.warn("Received error response: {}", error);
+ return "/mielecloud/failure?" + FailureServlet.OAUTH2_ERROR_PARAMETER_NAME + "=" + error;
+ }
+
+ String code = request.getParameter(CODE_PARAMETER_NAME);
+ if (code == null) {
+ logger.warn("Code is null");
+ return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
+ }
+ String state = request.getParameter(STATE_PARAMETER_NAME);
+ if (state == null) {
+ logger.warn("State is null");
+ return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
+ }
+
+ try {
+ ThingUID bridgeId = authorizationHandler.getBridgeUid();
+ String email = authorizationHandler.getEmail();
+
+ StringBuffer requestUrl = request.getRequestURL();
+ if (requestUrl == null) {
+ return "/mielecloud/failure?" + FailureServlet.MISSING_REQUEST_URL_PARAMETER_NAME + "=true";
+ }
+
+ try {
+ authorizationHandler.completeAuthorization(requestUrl.toString() + "?" + request.getQueryString());
+ } catch (OAuthException e) {
+ logger.warn("Failed to complete authorization.", e);
+ return "/mielecloud/failure?" + FailureServlet.FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME
+ + "=true";
+ }
+
+ return "/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeId.getAsString()
+ + "&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+ } catch (NoOngoingAuthorizationException e) {
+ logger.warn("Failed to complete authorization: There is no ongoing authorization or it timed out");
+ return "/mielecloud/failure?" + FailureServlet.NO_ONGOING_AUTHORIZATION_PARAMETER_NAME + "=true";
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java
new file mode 100644
index 0000000000000..4441aca3d8d8a
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for common servlet tasks.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ServletUtil {
+ private ServletUtil() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the value of a request parameter or returns a default if the parameter is not present.
+ */
+ public static String getParameterValueOrDefault(HttpServletRequest request, String parameterName,
+ String defaultValue) {
+ String parameterValue = request.getParameter(parameterName);
+ if (parameterValue == null) {
+ return defaultValue;
+ } else {
+ return parameterValue;
+ }
+ }
+
+ /**
+ * Checks whether a request parameter is enabled.
+ */
+ public static boolean isParameterEnabled(HttpServletRequest request, String parameterName) {
+ return "true".equalsIgnoreCase(getParameterValueOrDefault(request, parameterName, "false"));
+ }
+
+ /**
+ * Checks whether a parameter is present in a request.
+ */
+ public static boolean isParameterPresent(HttpServletRequest request, String parameterName) {
+ String parameterValue = request.getParameter(parameterName);
+ return parameterValue != null && !parameterValue.trim().isEmpty();
+ }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java
new file mode 100644
index 0000000000000..d240f215ee738
--- /dev/null
+++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet showing the success page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class SuccessServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = 7013060161686096950L;
+
+ public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
+ public static final String EMAIL_PARAMETER_NAME = "email";
+
+ public static final String BRIDGE_CREATION_FAILED_PARAMETER_NAME = "bridgeCreationFailed";
+ public static final String BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME = "bridgeReconfigurationFailed";
+
+ private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "";
+ private static final String BRIDGE_UID_PLACEHOLDER = "";
+ private static final String EMAIL_PLACEHOLDER = "";
+ private static final String THINGS_TEMPLATE_CODE_PLACEHOLDER = "";
+
+ private static final String LOCALE_OPTIONS_PLACEHOLDER = "";
+
+ private static final String DEFAULT_LANGUAGE = "en";
+ private static final Set SUPPORTED_LANGUAGES = Set.of("da", "nl", "en", "fr", "de", "it", "nb", "es");
+
+ private final Logger logger = LoggerFactory.getLogger(SuccessServlet.class);
+
+ private final LanguageProvider languageProvider;
+ private final ThingsTemplateGenerator templateGenerator;
+
+ /**
+ * Creates a new {@link SuccessServlet}.
+ *
+ * @param resourceLoader Loader for resources.
+ * @param languageProvider Provider for the language to use as default selection.
+ */
+ public SuccessServlet(ResourceLoader resourceLoader, LanguageProvider languageProvider) {
+ super(resourceLoader);
+ this.languageProvider = languageProvider;
+ this.templateGenerator = new ThingsTemplateGenerator();
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
+ if (bridgeUidString == null || bridgeUidString.isEmpty()) {
+ logger.warn("Success page is missing bridge UID.");
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Missing bridge UID.");
+ }
+
+ String email = request.getParameter(EMAIL_PARAMETER_NAME);
+ if (email == null || email.isEmpty()) {
+ logger.warn("Success page is missing e-mail address.");
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Missing e-mail address.");
+ }
+
+ ThingUID bridgeUid = null;
+ try {
+ bridgeUid = new ThingUID(bridgeUidString);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Success page received malformed bridge UID '{}'.", bridgeUidString);
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Malformed bridge UID.");
+ }
+
+ if (!EmailValidator.isValid(email)) {
+ logger.warn("Success page received malformed e-mail address '{}'.", email);
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Malformed e-mail address.");
+ }
+
+ String skeleton = getResourceLoader().loadResourceAsString("success.html");
+ skeleton = renderErrorMessage(request, skeleton);
+ skeleton = renderBridgeUid(skeleton, bridgeUid);
+ skeleton = renderEmail(skeleton, email);
+ skeleton = renderLocaleSelection(skeleton);
+ skeleton = renderBridgeConfigurationTemplate(skeleton, bridgeUid, email);
+ return skeleton;
+ }
+
+ private String renderErrorMessage(HttpServletRequest request, String skeleton) {
+ if (ServletUtil.isParameterEnabled(request, BRIDGE_CREATION_FAILED_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again.
");
+ } else if (ServletUtil.isParameterEnabled(request, BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again.
");
+ } else {
+ return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, "");
+ }
+ }
+
+ private String renderBridgeUid(String skeleton, ThingUID bridgeUid) {
+ return skeleton.replace(BRIDGE_UID_PLACEHOLDER, bridgeUid.getAsString());
+ }
+
+ private String renderEmail(String skeleton, String email) {
+ return skeleton.replace(EMAIL_PLACEHOLDER, email);
+ }
+
+ private String renderLocaleSelection(String skeleton) {
+ String preSelectedLanguage = languageProvider.getLanguage().filter(SUPPORTED_LANGUAGES::contains)
+ .orElse(DEFAULT_LANGUAGE);
+
+ return skeleton.replace(LOCALE_OPTIONS_PLACEHOLDER,
+ SUPPORTED_LANGUAGES.stream().map(Language::fromCode).filter(Optional::isPresent).map(Optional::get)
+ .sorted()
+ .map(language -> createOptionTag(language, preSelectedLanguage.equals(language.getCode())))
+ .collect(Collectors.joining("\n")));
+ }
+
+ private String createOptionTag(Language language, boolean selected) {
+ String firstPart = "