diff --git a/bom/pom.xml b/bom/pom.xml index 411783b..5217c73 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1,4 +1,5 @@ - 4.0.0 @@ -10,4 +11,56 @@ portal ui bom Defines all portal-ui modules pom + + + + de.cuioss.portal.ui + portal-ui-api + ${project.version} + compile + + + de.cuioss.portal.ui + portal-ui-runtime + ${project.version} + runtime + + + de.cuioss.portal.ui + portal-ui-default-navigation + ${project.version} + runtime + + + de.cuioss.portal.ui + portal-ui-oauth + ${project.version} + runtime + + + de.cuioss.portal.ui + portal-ui-errorpages + ${project.version} + runtime + + + de.cuioss.portal.ui + portal-ui-form-based-login + ${project.version} + runtime + + + de.cuioss.portal.ui + portal-ui-bootstrap-page-templates + ${project.version} + runtime + + + de.cuioss.portal.ui + portal-ui-unit-testing + ${project.version} + test + + + \ No newline at end of file diff --git a/configuration/pom.xml b/configuration/pom.xml new file mode 100644 index 0000000..d122f15 --- /dev/null +++ b/configuration/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + de.cuioss.portal.ui + cui-portal-ui + 1.0.0-SNAPSHOT + + configuration + pom + Portal Configuration + Aggregates the the configuration specific artifacts + + portal-ui-default-navigation + + \ No newline at end of file diff --git a/configuration/portal-ui-default-navigation/pom.xml b/configuration/portal-ui-default-navigation/pom.xml new file mode 100644 index 0000000..c2a001d --- /dev/null +++ b/configuration/portal-ui-default-navigation/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + de.cuioss.portal.ui + configuration + 1.0.0-SNAPSHOT + + portal-ui-default-navigation + Wraps a faces-config containing the default navigation for the excluding 'home'. Usually only needed for mock/test-deployments + \ No newline at end of file diff --git a/configuration/portal-ui-default-navigation/src/main/resources/META-INF/cuioss-portal/cuioss-portal-default-navigation.faces-config.xml b/configuration/portal-ui-default-navigation/src/main/resources/META-INF/cuioss-portal/cuioss-portal-default-navigation.faces-config.xml new file mode 100644 index 0000000..ad26ef4 --- /dev/null +++ b/configuration/portal-ui-default-navigation/src/main/resources/META-INF/cuioss-portal/cuioss-portal-default-navigation.faces-config.xml @@ -0,0 +1,38 @@ + + + CuiossPortalDefaultNavigation + + * + + error + /faces/guest/error.xhtml + + + + + * + + preferences + /faces/pages/account/preferences.xhtml + + + + + * + + account + /faces/pages/account/account.xhtml + + + + + * + + about + /faces/pages/account/about.xhtml + + + + \ No newline at end of file diff --git a/modules/pom.xml b/modules/pom.xml index c6fe2d8..c35e799 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -1,11 +1,159 @@ - - 4.0.0 - - de.cuioss.portal.ui - cui-portal-ui - 1.0.0-SNAPSHOT - - modules - pom - portal ui modules + + 4.0.0 + + de.cuioss.portal.ui + cui-portal-ui + 1.0.0-SNAPSHOT + + modules + pom + portal ui modules + + 1.0.0-SNAPSHOT + 0.2.7 + 1.0.0-SNAPSHOT + 1.1.0-SNAPSHOT + + + portal-ui-api + portal-ui-runtime + + portal-ui-oauth + portal-ui-errorpages + portal-ui-form-based-login + portal-ui-unit-testing + portal-ui-bootstrap-page-templates + + + + + de.cuioss.test + cui-jsf-test-basic + ${version.cui.test.jsf.basic} + test + + + de.cuioss.portal.ui + bom + ${project.version} + pom + import + + + + de.cuioss + java-ee-8-bom + ${version.cui.parent} + pom + import + + + de.cuioss + java-ee-orthogonal + ${version.cui.parent} + pom + import + + + de.cuioss.jsf + bom + ${version.cui.jsf.components} + pom + import + + + de.cuioss.portal + bom + ${version.cui.portal.core} + pom + import + + + + + + + org.projectlombok + lombok + + + + org.apache.myfaces.core + myfaces-api + + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.websocket + jakarta.websocket-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.el + jakarta.el-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + jakarta.annotation + jakarta.annotation-api + + + org.eclipse.microprofile.config + microprofile-config-api + + + + + + de.cuioss + cui-java-tools + + + + + de.cuioss.portal.test + portal-core-unit-testing + + + de.cuioss.test + cui-jsf-test-basic + + + de.cuioss.test + cui-test-juli-logger + + + de.cuioss.test + cui-test-value-objects + + + org.junit.jupiter + junit-jupiter + + + de.cuioss.portal.configuration + portal-configuration-impl + test + + + org.jboss.weld + weld-junit5 + + + org.slf4j + slf4j-api + test + + \ No newline at end of file diff --git a/modules/portal-ui-api/pom.xml b/modules/portal-ui-api/pom.xml new file mode 100644 index 0000000..255854f --- /dev/null +++ b/modules/portal-ui-api/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + de.cuioss.portal.ui + modules + 1.0.0-SNAPSHOT + + portal-ui-api + Portal UI API + Defines the api for cui-portal + + + + de.cuioss.jsf + cui-jsf-api + + + + de.cuioss.portal.configuration + portal-configuration-api + + + de.cuioss.portal.authentication + portal-authentication-api + + + de.cuioss.portal.core + portal-core + + + \ No newline at end of file diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/GlobalComponentIds.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/GlobalComponentIds.java new file mode 100644 index 0000000..a968f51 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/GlobalComponentIds.java @@ -0,0 +1,60 @@ +package de.cuioss.portal.ui.api; + +import static de.cuioss.tools.string.MoreStrings.requireNotEmpty; + +import lombok.Getter; + +/** + * Global component id's + * + * @author i000576 + * + */ +public enum GlobalComponentIds { + + /** + * Time out form identifier + */ + TIMEOUT_FORM("timeoutForm"), + + /** + * Top Navigation bar identifier + */ + NAVIGATION_BAR_TOP("navbarTop"), + + /** + * Top Navigation bar home link identifier + */ + NAVIGATION_BAR_TOP_HOME("navbarTopHome"), + + /** + * Global messages (Growl) identifier + */ + GLOBAL_PAGE_MESSAGES("globalPageMessages"), + + /** + * Main container identifer + */ + MAIN_CONTENT("container"), + + /** + * Login page username input field id + */ + LOGIN_PAGE_USER_NAME("username"), + + /** + * Login page password input field id + */ + LOGIN_PAGE_USER_PASSWORD("password"); + + /** + * Component string identifier + */ + @Getter + private final String id; + + GlobalComponentIds(final String idValue) { + id = requireNotEmpty(idValue, "idValue must not be null or empty"); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/PortalCoreBeanNames.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/PortalCoreBeanNames.java new file mode 100644 index 0000000..fcbc3f2 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/PortalCoreBeanNames.java @@ -0,0 +1,44 @@ +package de.cuioss.portal.ui.api; + +import de.cuioss.portal.core.storage.ClientStorage; +import de.cuioss.portal.core.storage.SessionStorage; +import de.cuioss.portal.ui.api.message.StickyMessageProducer; +import de.cuioss.portal.ui.api.templating.MultiTemplatingMapper; +import de.cuioss.portal.ui.api.templating.MultiViewMapper; +import lombok.experimental.UtilityClass; + +/** + * Container for static bean-names. Usually needed for access from views. In + * other cases CDI should be used directly. + * + * @author Oliver Wolff + * + */ +@UtilityClass +public class PortalCoreBeanNames { + + /** + * Bean name for looking up instances of {@link ClientStorage}. + */ + public static final String CLIENT_STORAGE_BEAN_NAME = "portalClientStorage"; + + /** + * Bean name for looking up instances of {@link SessionStorage}. + */ + public static final String SESSION_STORAGE_BEAN_NAME = "portalSessionStorage"; + + /** + * Bean name for looking up instances of {@link StickyMessageProducer}. + */ + public static final String STICKY_MESSAGE_BEAN_NAME = "stickyMessageProducer"; + + /** + * Bean name for looking up instances of {@link MultiTemplatingMapper}. + */ + public static final String MULTI_TEMPLATING_MAPPER_BEAN_NAME = "multiTemplatingMapper"; + + /** + * Bean name for looking up instances of {@link MultiViewMapper}. + */ + public static final String MULTI_VIEW_MAPPER_BEAN_NAME = "multiViewMapper"; +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/UserNotAuthenticatedException.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/UserNotAuthenticatedException.java new file mode 100644 index 0000000..b94e012 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/UserNotAuthenticatedException.java @@ -0,0 +1,15 @@ +package de.cuioss.portal.ui.api.authentication; + +import de.cuioss.portal.authentication.AuthenticatedUserInfo; + +/** + * To be fired if a user is not authenticated. It will be derived from + * {@link AuthenticatedUserInfo#isAuthenticated()} + * + * @author Oliver Wolff + */ +public class UserNotAuthenticatedException extends RuntimeException { + + private static final long serialVersionUID = -587461529456917746L; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/UserNotAuthorizedException.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/UserNotAuthorizedException.java new file mode 100644 index 0000000..d212664 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/UserNotAuthorizedException.java @@ -0,0 +1,37 @@ +package de.cuioss.portal.ui.api.authentication; + +import java.util.Collection; + +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * To be fired if a user is not authorized for a certain view. It will be + * derived from and {@link AuthenticatedUserInfo#getRoles()} + * + * @author Matthias Walliczek + */ +@RequiredArgsConstructor +public class UserNotAuthorizedException extends RuntimeException { + + private static final long serialVersionUID = -2144266323935586941L; + + /** The view that is requested */ + @Getter + @NonNull + private final ViewDescriptor requestedView; + + /** The required roles to access the views. */ + @Getter + @NonNull + private final Collection requiredRoles; + + /** The roles the user provides. */ + @Getter + @NonNull + private final Collection userRoles; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/package-info.java new file mode 100644 index 0000000..6747d8c --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/authentication/package-info.java @@ -0,0 +1,7 @@ +/** + * Intermediate approach on how to define which pages needs to be secured. Later + * on it will be replaced by delta-spike mechanism. + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.authentication; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/PortalNotConfiguredException.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/PortalNotConfiguredException.java new file mode 100644 index 0000000..ac59c66 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/PortalNotConfiguredException.java @@ -0,0 +1,11 @@ +package de.cuioss.portal.ui.api.configuration; + +/** + * To be fired if the portal is not configured yet. + * + * @author Sven Haag, Sven Haag + */ +public class PortalNotConfiguredException extends RuntimeException { + + private static final long serialVersionUID = -7258126465401588196L; +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/package-info.java new file mode 100644 index 0000000..9b63444 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/package-info.java @@ -0,0 +1,39 @@ +/** + *

+ * Provides a unified view on the the web.xml based configuration of the + * CDI-portal. The configuration of the CDI-portal can be done in two ways: + *

+ *

web.xml

+ *

+ * This is the simple way to configure the portal by using entries in the + * web.xml, the corresponding keys / descriptions can be found within + * {@link de.icw.cui.portal.configuration.PortalConfigurationKeys}. This should + * suffice the most needs. + *

+ *

CDI-Way

+ *

+ * For some cased the file based may not be enough. Therefore you can override + * the corresponding services providing the corresponding configuration: + *

    + *
  • Resource Configuration: Provide an instance of + * {@link com.icw.ehf.cui.application.resources.CuiResourceConfiguration} + * annotated with + * {@link de.cuioss.portal.ui.api.configuration.PortalResourceConfiguration}
  • + *
  • Theme Configuration: Provide an instance of + * {@link com.icw.ehf.cui.core.api.application.theme.ThemeConfiguration} + * annotated with + * {@link de.cuioss.portal.ui.api.theme.PortalThemeConfiguration}
  • + *
  • History Configuration: Provide an instance of + * {@link com.icw.ehf.cui.application.history.HistoryConfiguration} annotated + * with {@link de.cuioss.portal.ui.api.history.PortalHistoryConfiguration}
  • + *
+ *

+ *

Advanced CDI

+ *

+ * If you want to replace the complete mechanism of web.xml configuration you + * need only to create an alternate producer for + * {@link com.icw.ehf.cui.cdi.api.context.CuiInitParameterMap} containing all + * corresponding parameter. + *

+ */ +package de.cuioss.portal.ui.api.configuration; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/types/ConfigAsViewMatcher.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/types/ConfigAsViewMatcher.java new file mode 100644 index 0000000..3638718 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/types/ConfigAsViewMatcher.java @@ -0,0 +1,44 @@ +package de.cuioss.portal.ui.api.configuration.types; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.util.Nonbinding; +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.application.view.matcher.EmptyViewMatcher; +import de.cuioss.jsf.api.application.view.matcher.ViewMatcher; +import de.cuioss.portal.configuration.PortalConfigurationKeys; + +/** + * Injects a config property as a {@link ViewMatcher}. In case the the property + * is null or empty it will be an {@link EmptyViewMatcher}. The default + * splitting character for the individual paths is + * {@value PortalConfigurationKeys#CONTEXT_PARAM_SEPARATOR} + * + * @author Oliver Wolff + */ +@Qualifier +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +@Retention(RUNTIME) +public @interface ConfigAsViewMatcher { + + /** + * @return the name of the property + */ + @Nonbinding + String name(); + + /** + * @return the separator char, defaults to + * {@value PortalConfigurationKeys#CONTEXT_PARAM_SEPARATOR} + */ + @Nonbinding + char separator() default PortalConfigurationKeys.CONTEXT_PARAM_SEPARATOR; +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/types/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/types/package-info.java new file mode 100644 index 0000000..fef0aeb --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/configuration/types/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides types converter for the deltaspike based configuration system + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.configuration.types; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/BaseLazyLoadingListItemWidget.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/BaseLazyLoadingListItemWidget.java new file mode 100644 index 0000000..2bb9e7d --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/BaseLazyLoadingListItemWidget.java @@ -0,0 +1,40 @@ +package de.cuioss.portal.ui.api.dashboard; + +import java.util.Collections; +import java.util.List; + +import de.cuioss.jsf.api.components.model.widget.DashboardWidgetModel; +import de.cuioss.jsf.api.components.model.widget.ListItem; +import de.cuioss.jsf.api.components.model.widget.ListItemWidgetModel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = true) +@ToString +public abstract class BaseLazyLoadingListItemWidget extends BaseLazyLoadingWidget + implements ListItemWidgetModel, DashboardWidgetModel { + + private static final long serialVersionUID = -9216862082387228019L; + + @Getter + private List items = Collections.emptyList(); + + /** + * @return the id of the in this module defined composite component that should + * be used as default for this implementations of this abstract widget + * class. May be overridden by a different id of a more specific + * composite component. + */ + @Override + public String getCompositeComponentId() { + return "cui-composite:listItemWidget"; + } + + protected abstract List mapResult(T result); + + @Override + public void handleResult(T result) { + items = mapResult(result); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/BaseLazyLoadingWidget.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/BaseLazyLoadingWidget.java new file mode 100644 index 0000000..ae86d82 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/BaseLazyLoadingWidget.java @@ -0,0 +1,62 @@ +package de.cuioss.portal.ui.api.dashboard; + +import javax.faces.event.ActionEvent; +import javax.inject.Inject; + +import de.cuioss.jsf.api.components.css.ContextState; +import de.cuioss.jsf.api.components.model.lazyloading.LazyLoadingThreadModel; +import de.cuioss.jsf.api.components.model.widget.BaseWidget; +import de.cuioss.portal.ui.api.ui.lazyloading.LazyLoadingRequest; +import de.cuioss.portal.ui.api.ui.lazyloading.LazyLoadingViewController; +import de.cuioss.uimodel.nameprovider.IDisplayNameProvider; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ToString +@EqualsAndHashCode(callSuper = false) +public abstract class BaseLazyLoadingWidget extends BaseWidget implements LazyLoadingRequest { + + private static final long serialVersionUID = -3234472642651082710L; + + @Inject + private LazyLoadingThreadModel viewModel; + + @Inject + private LazyLoadingViewController viewController; + + @Override + public void startInitialize() { + viewController.startRequest(this); + } + + @Override + public long getRequestId() { + return viewModel.getRequestId(); + } + + @Override + public IDisplayNameProvider getNotificationBoxValue() { + return viewModel.getNotificationBoxValue(); + } + + @Override + public ContextState getNotificationBoxState() { + return viewModel.getNotificationBoxState(); + } + + @Override + public boolean isInitialized() { + return viewModel.isInitialized(); + } + + @Override + public boolean isRenderContent() { + return viewModel.isRenderContent(); + } + + @Override + public void processAction(ActionEvent actionEvent) { + viewModel.processAction(actionEvent); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/PortalDashboardWidget.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/PortalDashboardWidget.java new file mode 100644 index 0000000..a057463 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/dashboard/PortalDashboardWidget.java @@ -0,0 +1,25 @@ +package de.cuioss.portal.ui.api.dashboard; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Defines Instances of + * {@link com.icw.ehf.cui.core.api.components.model.widget.DashboardWidgetModel} + * + * @author Matthias Walliczek + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalDashboardWidget { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/events/PageRefreshEvent.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/events/PageRefreshEvent.java new file mode 100644 index 0000000..7ddec2e --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/events/PageRefreshEvent.java @@ -0,0 +1,23 @@ +package de.cuioss.portal.ui.api.events; + +import java.io.Serializable; + +import lombok.Data; + +/** + * Page refresh event payload. + * + * @author i000576 + */ +@Data +public class PageRefreshEvent implements Serializable { + + private static final long serialVersionUID = 3367481133773296933L; + + /** + * the String based identifier of the current view-id. It ends on the real + * (physical) suffix, e.g. .xhtml + */ + private final String viewId; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/exceptions/DefaultErrorMessage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/exceptions/DefaultErrorMessage.java new file mode 100644 index 0000000..6457871 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/exceptions/DefaultErrorMessage.java @@ -0,0 +1,62 @@ +package de.cuioss.portal.ui.api.exceptions; + +import java.io.Serializable; + +import de.cuioss.portal.core.storage.MapStorage; +import de.cuioss.portal.core.storage.SessionStorage; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Simple Container for communicating and displaying exception information at + * the ui. + * + * @author Oliver Wolff + */ +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +public class DefaultErrorMessage implements Serializable { + + /** + * The lookup key for identifying error-messages within portal specific + * {@link SessionStorage} + */ + public static final String LOOKUP_KEY = "defaultErrorMessage"; + + /** Serial version UID. */ + private static final long serialVersionUID = 5486339618618279597L; + + /** The error code. */ + @Getter + private final String errorCode; + + /** The error code. */ + @Getter + private final String errorTicket; + + /** The error code. */ + @Getter + private final String errorMessage; + + /** The page that raised the error. */ + @Getter + private final String pageId; + + @Getter + private String stackTrace; + + /** + * Store a given {@link DefaultErrorMessage} into the given {@link MapStorage} + * + * @param errorMessage must not be null + * @param storage m,sut not be null + */ + public static final void addErrorMessageToSessionStorage(final DefaultErrorMessage errorMessage, + final MapStorage storage) { + storage.put(LOOKUP_KEY, errorMessage); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/exceptions/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/exceptions/package-info.java new file mode 100644 index 0000000..df8945d --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/exceptions/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides methods and structure user for handling of exceptions + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.exceptions; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/history/PortalHistoryManager.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/history/PortalHistoryManager.java new file mode 100644 index 0000000..117dba0 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/history/PortalHistoryManager.java @@ -0,0 +1,26 @@ +package de.cuioss.portal.ui.api.history; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.application.history.HistoryManager; + +/** + * Marker identifying concrete instances of {@link HistoryManager}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalHistoryManager { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/history/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/history/package-info.java new file mode 100644 index 0000000..1565e95 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/history/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides Interfaces and classes needed for the Configuration of the + * {@link com.icw.ehf.cui.application.history.impl.HistoryManagerBean} and + * therefore + * {@link com.icw.ehf.cui.application.history.HistoryNavigationHandler} + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.history; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/PhaseExecution.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/PhaseExecution.java new file mode 100644 index 0000000..9229dff --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/PhaseExecution.java @@ -0,0 +1,31 @@ +package de.cuioss.portal.ui.api.listener.view; + +import javax.faces.context.FacesContext; +import javax.faces.event.PhaseListener; + +/** + * Determines whether a concrete Portal-listener is to be fired + * {@link PhaseListener#beforePhase(javax.faces.event.PhaseEvent)} of + * {@link PhaseListener#afterPhase(javax.faces.event.PhaseEvent)} + * + * @author Oliver Wolff + */ +public enum PhaseExecution { + /** + * to be fired {@link PhaseListener#beforePhase(javax.faces.event.PhaseEvent)} + */ + BEFORE_PHASE, + + /** + * to be fired {@link PhaseListener#afterPhase(javax.faces.event.PhaseEvent)}. + * It will be called for {@link FacesContext#isPostback()} calls as well. + */ + AFTER_PHASE, + + /** + * To be fired {@link PhaseListener#afterPhase(javax.faces.event.PhaseEvent)} + * for non {@link FacesContext#isPostback()} calls. + */ + AFTER_PHASE_EXCLUDE_POSTBACK + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/PortalRestoreViewListener.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/PortalRestoreViewListener.java new file mode 100644 index 0000000..bb2bf2c --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/PortalRestoreViewListener.java @@ -0,0 +1,34 @@ +package de.cuioss.portal.ui.api.listener.view; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.faces.event.PhaseId; +import javax.faces.event.PhaseListener; +import javax.inject.Qualifier; + +/** + * Marker identifying concrete instances of {@link ViewListener}, that are to be + * fired at {@link PhaseId#RESTORE_VIEW} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalRestoreViewListener { + + /** + * @return {@link PhaseExecution} indicating whether a concrete Portal-listener + * is to be fired + * {@link PhaseListener#beforePhase(javax.faces.event.PhaseEvent)} of + * {@link PhaseListener#afterPhase(javax.faces.event.PhaseEvent)} + */ + PhaseExecution value(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/ViewListener.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/ViewListener.java new file mode 100644 index 0000000..623c1ef --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/ViewListener.java @@ -0,0 +1,29 @@ +package de.cuioss.portal.ui.api.listener.view; + +import java.io.Serializable; + +import javax.faces.event.PhaseId; + +import de.cuioss.jsf.api.common.view.ViewDescriptor; + +/** + * Instances of this Listener will be called from the portal as listener for + * {@link PhaseId#RESTORE_VIEW}. whether before or after is defined by + * {@link PhaseExecution}. It will pass the current viewId. + * + * @author Oliver Wolff + */ +public interface ViewListener extends Serializable { + + /** + * Command pattern like handler for interacting on a given view. This may be + * security checks, or sanity checks, e.g. The handler method must explicitly + * throw an exception or fire an event in order to act. + * + * @param viewDescriptor identifying the requested view. Must not be null nor + * empty. + */ + void handleView(ViewDescriptor viewDescriptor); + + boolean isEnabled(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/package-info.java new file mode 100644 index 0000000..ad27245 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/listener/view/package-info.java @@ -0,0 +1,12 @@ +/** + * Instances of {@link de.cuioss.portal.ui.api.listener.view.ViewListener} + * marked with + * {@link de.cuioss.portal.ui.api.listener.view.PortalRestoreViewListener} or + * {@link de.cuioss.portal.ui.api.listener.view.PortalAfterRestoreViewListener} + * are to be picked up and processed by the general portal restore View + * listener, see de.icw.cui.portal.configuration.application.listener.view for + * implementation details. + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.listener.view; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/LocaleChangeEvent.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/LocaleChangeEvent.java new file mode 100644 index 0000000..54d05bc --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/LocaleChangeEvent.java @@ -0,0 +1,26 @@ +package de.cuioss.portal.ui.api.locale; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Defines events that will be fired on LocaleChange, usually fired by + * {@link LocaleResolverService#saveUserLocale(java.util.Locale)} Its payload is + * Locale newLocale + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface LocaleChangeEvent { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/LocaleResolverService.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/LocaleResolverService.java new file mode 100644 index 0000000..0934575 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/LocaleResolverService.java @@ -0,0 +1,29 @@ +package de.cuioss.portal.ui.api.locale; + +import java.util.List; +import java.util.Locale; + +import de.cuioss.jsf.api.application.locale.LocaleProducer; + +/** + * Extension to {@link LocaleProducer} that adds additional methods like access + * on configured locales and changing the locale on per user basis. + * + * @author Oliver Wolff + */ +public interface LocaleResolverService extends LocaleProducer { + + /** + * @return the list of available locales for the current user. + */ + + List getAvailableLocales(); + + /** + * Saves the locale changed by user interaction + * + * @param locale to be updated. Must be one of {@link #getAvailableLocales()}. + * Otherwise it will throws an {@link IllegalArgumentException} + */ + void saveUserLocale(Locale locale); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/PortalLocaleResolver.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/PortalLocaleResolver.java new file mode 100644 index 0000000..198107d --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/PortalLocaleResolver.java @@ -0,0 +1,23 @@ +package de.cuioss.portal.ui.api.locale; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Defines Instances of {@link LocaleResolverService} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalLocaleResolver { +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/package-info.java new file mode 100644 index 0000000..e6a2ff1 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/locale/package-info.java @@ -0,0 +1,16 @@ +/** + * Defines ways to unify the handling of locales. + * {@link de.cuioss.portal.ui.api.locale.LocaleResolverService} + *
    + *
  • {@link de.icw.cui.portal.locale.PortalLocale} acts as a marker for the + * concrete request scoped locale. the portal must provide a corresponding + * producer.
  • + *
  • {@link de.cuioss.portal.ui.api.locale.PortalLocaleResolver} identifies + * the injection points for implementations of + * {@link de.cuioss.portal.ui.api.locale.LocaleResolverService}
  • + *
  • {@link de.cuioss.portal.ui.api.locale.LocaleResolverService}: Defines the + * interaction part of dealing with locales. This is the intended extension + * point.
  • + *
+ */ +package de.cuioss.portal.ui.api.locale; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/NavigationMenuProvider.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/NavigationMenuProvider.java new file mode 100644 index 0000000..0056292 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/NavigationMenuProvider.java @@ -0,0 +1,57 @@ +package de.cuioss.portal.ui.api.menu; + +import java.io.Serializable; +import java.util.List; +import java.util.Optional; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItem; +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItemContainer; + +/** + * Provider for the content of the portal specific navigation menu + * + * @author Oliver Wolff + */ +public interface NavigationMenuProvider extends Serializable { + + /** + * @return the list of {@link NavigationMenuItem} representing the top level + * element of the navigation menu. + */ + List getNavigationMenuRoots(); + + /** + * @return boolean indicating whether to display the navigation menu at all. + */ + boolean isDisplayNavigationMenu(); + + /** + * Returns an {@link NavigationMenuItem} by id + * + * @param id may be null or empty + * @return the found {@link NavigationMenuItem} if present, + * {@link Optional#empty()} if id is {@code null} or none could be found + */ + Optional getMenuItemById(String id); + + List getMenuItemsByParentId(String parentId); + + /** + * Returns an {@link NavigationMenuItemContainer} by id + * + * @param id may be null or empty + * @return the found {@link NavigationMenuItemContainer} if present, + * {@link Optional#empty()} if id is {@code null} or none could be + * found, or the found id does not represent a container + */ + Optional getContainerMenuItemById(String id); + + /** + * Returns a List of {@link NavigationMenuItem} for the given Ids + * + * @param ids to be looked up + * @return the found {@link NavigationMenuItem} if present, an empty list + * otherwise + */ + List getMenuItemsByIds(String... ids); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalMenuItem.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalMenuItem.java new file mode 100644 index 0000000..a2d1b64 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalMenuItem.java @@ -0,0 +1,27 @@ +package de.cuioss.portal.ui.api.menu; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItem; + +/** + * Marker identifying instances of the {@link NavigationMenuItem} that will be + * autoregistered to instances of {@link NavigationMenuProvider}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalMenuItem { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuConfigParser.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuConfigParser.java new file mode 100644 index 0000000..2c08f5b --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuConfigParser.java @@ -0,0 +1,82 @@ +package de.cuioss.portal.ui.api.menu; + +import java.util.Map; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItem; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.tools.string.MoreStrings; +import lombok.Getter; + +/** + * Abstract class to implement {@link NavigationMenuItem#getOrder()} and + * {@link NavigationMenuItem#isRendered()} using a configuration map. + */ +public abstract class PortalNavigationMenuConfigParser { + + private static final CuiLogger log = new CuiLogger(PortalNavigationMenuConfigParser.class); + + private static final String ENABLED_SUFFIX = ".enabled"; + + private static final String ORDER_SUFFIX = ".order"; + + @Getter(lazy = true) + private final String parentId = resolveParentId(); + + @Getter(lazy = true) + private final Integer order = resolveOrder(); + + /** + * Evaluates the order to sort the menu items. + * + * See also + * {@link de.icw.cui.portal.configuration.PortalConfigurationKeys#MENU_BASE}. + * + * @return the order if set or -1 if order is not set. + */ + public Integer resolveOrder() { + if (MoreStrings.isEmpty(getMenuConfig().get(getId() + ORDER_SUFFIX))) { + return -1; + } + try { + return Integer.parseInt(getMenuConfig().get(getId() + ORDER_SUFFIX)); + } catch (NumberFormatException e) { + log.warn("Portal-138: Invalid menu configuration: Order property '" + + getMenuConfig().get(getId() + ORDER_SUFFIX) + "' for menu item '" + getId() + + "' can not be parsed", e); + return -1; + } + } + + protected abstract Map getMenuConfig(); + + protected abstract String getId(); + + /** + * Evaluates the rendered attributes + * + * Defaults to true if the .order config key is set and .enabled config key is + * not set or set to true. + * + * See also + * {@link de.icw.cui.portal.configuration.PortalConfigurationKeys#MENU_BASE}. + * + * @return true if the menu item should be rendered. + */ + public boolean isRendered() { + if (!getMenuConfig().containsKey(getId() + ORDER_SUFFIX) + || MoreStrings.isEmpty(getMenuConfig().get(getId() + ORDER_SUFFIX))) { + return false; + } + return !getMenuConfig().containsKey(getId() + ENABLED_SUFFIX) + || Boolean.parseBoolean(getMenuConfig().get(getId() + ENABLED_SUFFIX)); + } + + private String resolveParentId() { + var parentKey = getId() + ".parent"; + if (!getMenuConfig().containsKey(parentKey)) { + return null; + } + return getMenuConfig().get(parentKey); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemContainerImpl.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemContainerImpl.java new file mode 100644 index 0000000..e5d30c7 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemContainerImpl.java @@ -0,0 +1,26 @@ +package de.cuioss.portal.ui.api.menu; + +import java.util.ArrayList; +import java.util.List; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItem; +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItemContainer; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Oliver Wolff + * + */ +@EqualsAndHashCode(callSuper = true) +public class PortalNavigationMenuItemContainerImpl extends PortalNavigationMenuItemImplBase + implements NavigationMenuItemContainer { + + private static final long serialVersionUID = 2451583664984874108L; + + @Getter + @Setter + private List children = new ArrayList<>(); + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemExternalSingleImpl.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemExternalSingleImpl.java new file mode 100644 index 0000000..ec6ef53 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemExternalSingleImpl.java @@ -0,0 +1,29 @@ +package de.cuioss.portal.ui.api.menu; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItemExternalSingle; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Oliver Wolff + * + */ +@EqualsAndHashCode(callSuper = true) +public class PortalNavigationMenuItemExternalSingleImpl extends PortalNavigationMenuItemImplBase + implements NavigationMenuItemExternalSingle { + + private static final long serialVersionUID = -1489949340663388532L; + + @Getter + @Setter + private String hRef; + + @Getter + @Setter + private String target; + + @Getter + @Setter + private String onClickAction; +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemImplBase.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemImplBase.java new file mode 100644 index 0000000..d9ea5bf --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemImplBase.java @@ -0,0 +1,91 @@ +package de.cuioss.portal.ui.api.menu; + +import static de.cuioss.portal.configuration.PortalConfigurationKeys.MENU_BASE; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.faces.context.FacesContext; +import javax.inject.Inject; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItem; +import de.cuioss.jsf.api.components.support.LabelResolver; +import de.cuioss.portal.configuration.types.ConfigAsFilteredMap; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * Base implementation for any cdi portal based {@link NavigationMenuItem} + * implementation. + */ +@EqualsAndHashCode(callSuper = true) +public class PortalNavigationMenuItemImplBase extends PortalNavigationMenuConfigParser implements NavigationMenuItem { + + private static final long serialVersionUID = -7137939377092965593L; + + @Inject + @ConfigAsFilteredMap(startsWith = MENU_BASE, stripPrefix = true) + @Getter(value = AccessLevel.PROTECTED) + private Map menuConfig; + + @Getter + @Setter + private String id; + + @Getter + @Setter + private String iconStyleClass; + + /** @deprecated use {@link #isRendered()} instead */ + @Getter + @Setter + @Deprecated + private boolean disabled = false; + + @Getter + @Setter + private String titleKey; + + @Getter + @Setter + private String titleValue; + + @Getter + @Setter + private List activeForAdditionalViewId = new ArrayList<>(); + + @Override + public int compareTo(final NavigationMenuItem other) { + return getOrder().compareTo(other.getOrder()); + } + + @Override + public String getResolvedTitle() { + return getResolvedLabel(); + } + + @Override + public boolean isTitleAvailable() { + return null != getResolvedTitle(); + } + + @Getter + @Setter + private String labelKey; + + @Getter + @Setter + private String labelValue; + + /** + * @return the resolved label + */ + public String getResolvedLabel() { + return LabelResolver.builder().withLabelKey(labelKey).withLabelValue(labelValue).build() + .resolve(FacesContext.getCurrentInstance()); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemSingleImpl.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemSingleImpl.java new file mode 100644 index 0000000..2fa69e2 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemSingleImpl.java @@ -0,0 +1,57 @@ +package de.cuioss.portal.ui.api.menu; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Provider; + +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItemSingle; +import de.cuioss.portal.ui.api.ui.context.CuiCurrentView; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Implementation of {@link NavigationMenuItemSingle} using cdi portal and it's + * configuration. + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = false, doNotUseGetters = true) +public class PortalNavigationMenuItemSingleImpl extends PortalNavigationMenuItemImplBase + implements NavigationMenuItemSingle { + + private static final long serialVersionUID = -4639141255087105993L; + + @Inject + @CuiCurrentView + private Provider currentViewProvider; + + @Getter + @Setter + private String outcome; + + @Getter + @Setter + private String target; + + @Getter + @Setter + private String onClickAction; + + @Getter + private final Map outcomeParameter = new HashMap<>(); + + /** + * @return true if the current view id equals the configured outcome or is + * contained in {@link #getActiveForAdditionalViewId()}. + */ + @Override + public boolean isActive() { + return currentViewProvider.get().getViewId().contains(getOutcome()) || !getActiveForAdditionalViewId().isEmpty() + && getActiveForAdditionalViewId().contains(currentViewProvider.get().getViewId()); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/AboutMenuItem.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/AboutMenuItem.java new file mode 100644 index 0000000..2c563bb --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/AboutMenuItem.java @@ -0,0 +1,46 @@ +package de.cuioss.portal.ui.api.menu.items; + +import javax.enterprise.context.Dependent; + +import de.cuioss.portal.ui.api.menu.PortalMenuItem; +import de.cuioss.portal.ui.api.menu.PortalNavigationMenuItemSingleImpl; +import de.cuioss.portal.ui.api.ui.pages.AboutPage; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + *

+ * Default representation of the help menu item. The order is 48. It references + * {@link UserMenuItem#MENU_ID} as parentId and {@link AboutPage#OUTCOME} as + * outcome. + *

+ * + * @author Oliver Wolff + */ +@PortalMenuItem +@Dependent +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AboutMenuItem extends PortalNavigationMenuItemSingleImpl { + + private static final long serialVersionUID = 1452093785009425867L; + + /** The label Key for this component. */ + public static final String LABEL_KEY = "com.icw.ehf.commons.portal.menu.about.label"; + + /** The icon for this component. */ + public static final String ICON = "cui-icon-circle_question_mark"; + + /** The string based id for this menu item. */ + public static final String MENU_ID = "aboutMenuItem"; + + /** + * Constructor. + */ + public AboutMenuItem() { + super.setIconStyleClass(ICON); + super.setId(MENU_ID); + super.setLabelKey(LABEL_KEY); + super.setOutcome(AboutPage.OUTCOME); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/AccountMenuItem.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/AccountMenuItem.java new file mode 100644 index 0000000..44767cf --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/AccountMenuItem.java @@ -0,0 +1,46 @@ +package de.cuioss.portal.ui.api.menu.items; + +import javax.enterprise.context.Dependent; + +import de.cuioss.portal.ui.api.menu.PortalMenuItem; +import de.cuioss.portal.ui.api.menu.PortalNavigationMenuItemSingleImpl; +import de.cuioss.portal.ui.api.ui.pages.AccountPage; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + *

+ * Default representation of the account menu item. The order is 25. It + * references {@link UserMenuItem#MENU_ID} as parentId and + * {@link AccountPage#OUTCOME} as outcome. + *

+ * + * @author Oliver Wolff + */ +@PortalMenuItem +@Dependent +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AccountMenuItem extends PortalNavigationMenuItemSingleImpl { + + private static final long serialVersionUID = 1452093785009425867L; + + /** The label Key for this component. */ + public static final String LABEL_KEY = "com.icw.ehf.commons.portal.menu.account.label"; + + /** The icon for this component. */ + public static final String ICON = "cui-icon-keys"; + + /** The string based id for this menu item. */ + public static final String MENU_ID = "accountMenuItem"; + + /** + * Constructor. + */ + public AccountMenuItem() { + super.setIconStyleClass(ICON); + super.setId(MENU_ID); + super.setLabelKey(LABEL_KEY); + super.setOutcome(AccountPage.OUTCOME); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/LogoutMenuItem.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/LogoutMenuItem.java new file mode 100644 index 0000000..9a061b7 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/LogoutMenuItem.java @@ -0,0 +1,50 @@ +package de.cuioss.portal.ui.api.menu.items; + +import javax.enterprise.context.Dependent; + +import de.cuioss.portal.ui.api.menu.PortalMenuItem; +import de.cuioss.portal.ui.api.menu.PortalNavigationMenuItemSingleImpl; +import de.cuioss.portal.ui.api.ui.pages.LogoutPage; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + *

+ * Default representation of the logout menu item. The order is 48. It has + * null as parentId (top-level element) and + * {@link LogoutPage#OUTCOME} as outcome. + *

+ * + * @author Oliver Wolff + */ +@PortalMenuItem +@Dependent +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class LogoutMenuItem extends PortalNavigationMenuItemSingleImpl { + + private static final long serialVersionUID = -1265061305901788409L; + + /** The label Key for this component. */ + public static final String LABEL_KEY = "com.icw.ehf.commons.portal.menu.logout.label"; + + /** The title Key for this component. */ + public static final String TITLE_KEY = "com.icw.ehf.commons.portal.menu.logout.title"; + + /** The icon for this component. */ + public static final String ICON = "cui-icon-power"; + + /** The string based id for this menu item. */ + public static final String MENU_ID = "logoutMenuItem"; + + /** + * Constructor. + */ + public LogoutMenuItem() { + super.setIconStyleClass(ICON); + super.setId(MENU_ID); + super.setLabelKey(LABEL_KEY); + super.setTitleKey(TITLE_KEY); + super.setOutcome(LogoutPage.OUTCOME); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/PreferencesMenuItem.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/PreferencesMenuItem.java new file mode 100644 index 0000000..c634b7d --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/PreferencesMenuItem.java @@ -0,0 +1,50 @@ +package de.cuioss.portal.ui.api.menu.items; + +import javax.enterprise.context.Dependent; + +import de.cuioss.portal.ui.api.menu.PortalMenuItem; +import de.cuioss.portal.ui.api.menu.PortalNavigationMenuItemSingleImpl; +import de.cuioss.portal.ui.api.ui.pages.PreferencesPage; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + *

+ * Default representation of the preferences menu-item. The order is 20. It + * references {@link UserMenuItem#MENU_ID} as parentId and + * {@link PreferencesPage#OUTCOME} as outcome. + *

+ * + * @author Oliver Wolff + */ +@PortalMenuItem +@Dependent +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PreferencesMenuItem extends PortalNavigationMenuItemSingleImpl { + + private static final long serialVersionUID = 1452093785009425867L; + + /** The label Key for this component. */ + public static final String LABEL_KEY = "com.icw.ehf.commons.portal.menu.preferences.label"; + + /** The title Key for this component. */ + public static final String TITLE_KEY = "com.icw.ehf.commons.portal.menu.preferences.title"; + + /** The icon for this component. */ + public static final String ICON = "cui-icon-settings"; + + /** The string based id for this menu item. */ + public static final String MENU_ID = "preferencesMenuItem"; + + /** + * Constructor. + */ + public PreferencesMenuItem() { + super.setIconStyleClass(ICON); + super.setId(MENU_ID); + super.setLabelKey(LABEL_KEY); + super.setTitleKey(TITLE_KEY); + super.setOutcome(PreferencesPage.OUTCOME); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/UserMenuItem.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/UserMenuItem.java new file mode 100644 index 0000000..4e4d1eb --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/items/UserMenuItem.java @@ -0,0 +1,64 @@ +package de.cuioss.portal.ui.api.menu.items; + +import java.util.ResourceBundle; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.Dependent; +import javax.inject.Inject; + +import de.cuioss.jsf.api.components.model.menu.NavigationMenuItemContainerImpl; +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.PortalUser; +import de.cuioss.portal.core.bundle.PortalResourceBundle; +import de.cuioss.portal.ui.api.menu.PortalMenuItem; +import de.cuioss.portal.ui.api.menu.PortalNavigationMenuItemContainerImpl; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + *

+ * Default representation of the user menu. + *

+ * Caution: the using class must set + * {@link NavigationMenuItemContainerImpl#setLabelValue(java.lang.String)} + * because this is dynamically computed. + * + * @author Oliver Wolff + * @author Sven Haag + */ +@PortalMenuItem +@Dependent +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class UserMenuItem extends PortalNavigationMenuItemContainerImpl { + + private static final long serialVersionUID = 1452093785009425867L; + + private static final String USER_MENU_TITLE_KEY = "com.icw.ehf.commons.portal.menu.user.title"; + + /** The icon for the user. */ + public static final String ICON = "cui-icon-user"; + + /** The string based id for this menu item. */ + public static final String MENU_ID = "userMenuItem"; + + @Inject + @PortalUser + private AuthenticatedUserInfo userInfo; + + @Inject + @PortalResourceBundle + private ResourceBundle resourceBundle; + + /** + * Initializes the user by setting label-value and Title-value + */ + @PostConstruct + public void init() { + super.setIconStyleClass(ICON); + super.setId(MENU_ID); + setLabelValue(userInfo.getDisplayName()); + setTitleValue(String.format(resourceBundle.getString(USER_MENU_TITLE_KEY), userInfo.getDisplayName())); + + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/package-info.java new file mode 100644 index 0000000..278acbf --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/menu/package-info.java @@ -0,0 +1,16 @@ +/** + *

+ * Provides classes for enabling the dynamic navigation menu. The navigation + * menu can be adapted in two ways: + *

    + *
  • Provide additional + * {@link com.icw.ehf.cui.components.bootstrap.menu.model.NavigationMenuItem} + * annotated with {@link de.cuioss.portal.ui.api.menu.PortalMenuItem}. Hierarchy + * and order is defined by + * {@link de.icw.cui.portal.configuration.PortalConfigurationKeys#MENU_BASE}
  • + *
+ *

+ * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.menu; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/PortalMessageProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/PortalMessageProducer.java new file mode 100644 index 0000000..f93c576 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/PortalMessageProducer.java @@ -0,0 +1,26 @@ +package de.cuioss.portal.ui.api.message; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.application.message.MessageProducer; + +/** + * Marker for identifying portal specific instances of {@link MessageProducer} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalMessageProducer { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/PortalStickyMessageProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/PortalStickyMessageProducer.java new file mode 100644 index 0000000..7229eaf --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/PortalStickyMessageProducer.java @@ -0,0 +1,25 @@ +package de.cuioss.portal.ui.api.message; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker for identifying portal specific instances of + * {@link StickyMessageProducer} + * + * @author Matthias Walliczek + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalStickyMessageProducer { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessage.java new file mode 100644 index 0000000..00c068c --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessage.java @@ -0,0 +1,41 @@ +package de.cuioss.portal.ui.api.message; + +import java.io.Serializable; + +import de.cuioss.jsf.api.components.css.ContextState; +import de.cuioss.uimodel.nameprovider.IDisplayNameProvider; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +/** + * A sticky message consisting of dismissable flag, message string and state. + * + * @author Matthias Walliczek + */ +@Builder +@Data +@AllArgsConstructor +public class StickyMessage implements Serializable { + + private static final long serialVersionUID = -3226075374956046365L; + + /** + * if true, message could be removed by UI interaction + */ + private final boolean dismissable; + + /** + * {@linkplain ContextState} is required + */ + @NonNull + private final ContextState state; + + /** + * Message content as {@linkplain IDisplayNameProvider} is required + */ + @NonNull + private final IDisplayNameProvider message; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessageProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessageProducer.java new file mode 100644 index 0000000..1e34495 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessageProducer.java @@ -0,0 +1,92 @@ +package de.cuioss.portal.ui.api.message; + +import java.util.List; + +import de.cuioss.jsf.api.components.css.ContextState; + +/** + * Produces sticky messages to be displayed until the user dismisses them. + * + * @author Matthias Walliczek + */ +public interface StickyMessageProducer { + + /** + * Convenience Method for setting sticky info messages + * + * @param messageKey must no be null + * @param parameter Ellipses of Object Parameter for MessageFormat + */ + void setInfoMessage(String messageKey, Object... parameter); + + /** + * Convenience Method for setting sticky error messages + * + * @param messageKey must no be null + * @param parameter Ellipses of Object Parameter for MessageFormat + */ + void setErrorMessage(String messageKey, Object... parameter); + + /** + * Convenience Method for setting sticky warning messages + * + * @param messageKey must no be null + * @param parameter Ellipses of Object Parameter for MessageFormat + */ + void setWarningMessage(String messageKey, Object... parameter); + + /** + * Stores and displays a sticky message with given severity and messageKey. + * + * @param messageKey must no be null + * @param severity The Severity level of the Message, must not be null. + * @param parameter Ellipses of Object Parameter for MessageFormat + */ + void setMessage(String messageKey, ContextState severity, Object... parameter); + + /** + * Stores and displays a sticky message with given severity and messageString. + * + * @param messageString must no be null + * @param severity The Severity level of the Message, must not be null. + * @param parameter Ellipses of Object Parameter for MessageFormat + */ + void setMessageAsString(final String messageString, final ContextState severity, final Object... parameter); + + /** + * Add complete StickyMessage to list + * + * @param message {@linkplain StickyMessage} must not be {@code null} + */ + void addMessage(final StickyMessage message); + + /** + * Remove StickyMessage from list if still available in list + * + * @param message {@linkplain StickyMessage} must not be {@code null} + */ + void removeMessage(final StickyMessage message); + + /** + * @return the contained messages + */ + List getMessages(); + + /** + * @return {@code true} if at least one message is available, {@code false} + * otherwise + */ + default boolean isAnyMessageAvailable() { + return !getMessages().isEmpty(); + } + + /** + * Remove all stored StickyMessages + * + * @Deprecated because it will delete all messages without filtering. Please + * rethink usage of the {@link StickyMessageProducer} and consider + * using {@link StickyMessageProvider} + */ + @Deprecated + void clearStoredMessages(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessageProvider.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessageProvider.java new file mode 100644 index 0000000..e0e71fa --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/message/StickyMessageProvider.java @@ -0,0 +1,22 @@ +package de.cuioss.portal.ui.api.message; + +import java.io.Serializable; +import java.util.Set; + +/** + * A provider of StickyMessages. Each class which implement this get collected + * by StickyMessageCollectorViewListener, which does add the collected messages + * to session scoped bean named stickyMessageProducer + * + * @author i000576 + */ +public interface StickyMessageProvider extends Serializable { + + /** + * Retrieve StickyMessages which should be added to StickyMessageProducer. + * + * @return set which could be empty but never null. + */ + Set retrieveMessages(); + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/CacheableResource.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/CacheableResource.java new file mode 100644 index 0000000..cecf5f4 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/CacheableResource.java @@ -0,0 +1,54 @@ +package de.cuioss.portal.ui.api.resources; + +import java.util.HashMap; +import java.util.Map; + +import javax.faces.application.ResourceHandler; +import javax.faces.context.FacesContext; + +/** + * Abstract resource allowing basic caching functionality. + * + * @author Matthias Walliczek + */ +public abstract class CacheableResource extends CuiResource { + + private static final String HEADER_E_TAG = "ETag"; + + private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + + /** + * Generate a unique string for the current resource file version. + * + * @return a string unique for the current resource file version. + */ + protected abstract String getETag(); + + @Override + public Map getResponseHeaders() { + final Map responseHeaders = new HashMap<>(); + if (getETag() != null) { + responseHeaders.put(HEADER_E_TAG, getETag()); + } + return responseHeaders; + } + + @Override + public boolean userAgentNeedsUpdate(final FacesContext context) { + final var requestHeaders = context.getExternalContext().getRequestHeaderMap(); + + return (!requestHeaders.containsKey(HEADER_IF_NONE_MATCH) || !getETag().equals(requestHeaders.get(HEADER_IF_NONE_MATCH))); + } + + /** + * Create resource path to be appended after context path + */ + protected String determineResourcePath() { + final var path = new StringBuilder(ResourceHandler.RESOURCE_IDENTIFIER); + path.append('/'); + path.append(getLibraryName()); + path.append('/'); + path.append(getResourceName()); + return path.toString(); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/CuiResource.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/CuiResource.java new file mode 100644 index 0000000..96be73f --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/CuiResource.java @@ -0,0 +1,40 @@ +package de.cuioss.portal.ui.api.resources; + +import java.util.Objects; + +import javax.faces.application.Resource; +import javax.faces.context.FacesContext; + +import de.cuioss.jsf.api.application.resources.util.ResourceUtil; +import lombok.ToString; + +/** + * Base Resource using {@link ResourceUtil} to calculate + * {@link #getRequestPath()} and {@link #getURL()}. + * + * @author Matthias Walliczek + */ +@ToString +public abstract class CuiResource extends Resource { + + @Override + public String getRequestPath() { + return ResourceUtil.calculateRequestPath(getResourceName(), getLibraryName(), + FacesContext.getCurrentInstance()); + } + + @Override + public int hashCode() { + return Objects.hash(getResourceName(), getLibraryName()); + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof CuiResource) { + final var other = (CuiResource) obj; + return Objects.equals(getResourceName(), other.getResourceName()) + && Objects.equals(getLibraryName(), other.getLibraryName()); + } + return false; + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/NonCachableResource.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/NonCachableResource.java new file mode 100644 index 0000000..837071e --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/NonCachableResource.java @@ -0,0 +1,31 @@ +package de.cuioss.portal.ui.api.resources; + +import java.util.HashMap; +import java.util.Map; + +import javax.faces.context.FacesContext; + +/** + * Abstract resource disabling caching. + * + * @author Matthias Walliczek + */ +public abstract class NonCachableResource extends CuiResource { + + private static final String HEADER_ACCEPT = "Accept"; + private static final String HEADER_CACHE_CONTROL = "Cache-Control"; + + @Override + public Map getResponseHeaders() { + Map responseHeaders = new HashMap<>(); + responseHeaders.put(HEADER_ACCEPT, "public"); + responseHeaders.put(HEADER_CACHE_CONTROL, "max-age=0"); + return responseHeaders; + } + + @Override + public boolean userAgentNeedsUpdate(final FacesContext context) { + return true; + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/package-info.java new file mode 100644 index 0000000..456e9b1 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/resources/package-info.java @@ -0,0 +1,7 @@ +/** + * FIXME owolff -> Move to core-jsf: Only reason for JSF-dependency on spec + * module + * + * @author Matthias Walliczek + */ +package de.cuioss.portal.ui.api.resources; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/storage/PortalClientStorageAccessor.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/storage/PortalClientStorageAccessor.java new file mode 100644 index 0000000..6083346 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/storage/PortalClientStorageAccessor.java @@ -0,0 +1,28 @@ +package de.cuioss.portal.ui.api.storage; + +import static de.cuioss.portal.ui.api.PortalCoreBeanNames.CLIENT_STORAGE_BEAN_NAME; + +import de.cuioss.jsf.api.common.accessor.ManagedBeanAccessor; +import de.cuioss.portal.core.storage.ClientStorage; + +/** + * Helper class for accessing instances of {@link ClientStorage} within objects + * that are not under control of the MangedBeanFacility, e.g. Converter, + * validators, components. + * + * @author Matthias Walliczek + * @deprecated use CDI directly + */ +@Deprecated +public class PortalClientStorageAccessor extends ManagedBeanAccessor { + + private static final long serialVersionUID = 6941913636918722401L; + + /** + * Constructor + */ + public PortalClientStorageAccessor() { + super(CLIENT_STORAGE_BEAN_NAME, ClientStorage.class, true); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/storage/PortalSessionStorageAccessor.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/storage/PortalSessionStorageAccessor.java new file mode 100644 index 0000000..ee15e42 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/storage/PortalSessionStorageAccessor.java @@ -0,0 +1,27 @@ +package de.cuioss.portal.ui.api.storage; + +import static de.cuioss.portal.ui.api.PortalCoreBeanNames.SESSION_STORAGE_BEAN_NAME; + +import de.cuioss.jsf.api.common.accessor.ManagedBeanAccessor; +import de.cuioss.portal.core.storage.SessionStorage; + +/** + * Helper class for accessing instances of {@link SessionStorage} within objects + * that are not under control of the MangedBeanFacility, e.g. Converter, + * validators, components. + * + * @author Oliver Wolff + * @deprecated use CDI directly + */ +@Deprecated +public class PortalSessionStorageAccessor extends ManagedBeanAccessor { + + private static final long serialVersionUID = 6941913636918722401L; + + /** + * Constructor + */ + public PortalSessionStorageAccessor() { + super(SESSION_STORAGE_BEAN_NAME, SessionStorage.class, true); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/MultiTemplatingMapper.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/MultiTemplatingMapper.java new file mode 100644 index 0000000..eed8eea --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/MultiTemplatingMapper.java @@ -0,0 +1,22 @@ +package de.cuioss.portal.ui.api.templating; + +import java.io.Serializable; +import java.net.URL; + +/** + * See package-info for description. + * + * @author Oliver Wolff + */ +public interface MultiTemplatingMapper extends Serializable { + + /** + * @param requestedResource must not be null. Represents a concrete template + * e.g. root.xhtml or subdirectory/component.xhtml + * without the technical path segments + * @return an instance of a {@link URL} to access the prefixed resource either + * as external file or as classpath resource, e.g. portal/root.xhtml or + * portal/subdirectory/component.xhtml respectively + */ + URL resolveTemplatePath(String requestedResource); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/MultiViewMapper.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/MultiViewMapper.java new file mode 100644 index 0000000..b1c8a3e --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/MultiViewMapper.java @@ -0,0 +1,23 @@ +package de.cuioss.portal.ui.api.templating; + +import java.io.Serializable; +import java.net.URL; + +/** + * See package-info for description. + * + * @author Matthias Walliczek + */ +public interface MultiViewMapper extends Serializable { + + /** + * @param requestedResource must not be null. Represents a concrete view e.g. + * faces/guest/login.xhtml or + * subdirectory/component.xhtml without the technical + * path segments + * @return an instance of a {@link URL} to access the prefixed resource either + * as external file or as classpath resource, e.g. portal/root.xhtml or + * portal/subdirectory/component.xhtml respectively + */ + URL resolveViewPath(String requestedResource); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalMultiTemplatingMapper.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalMultiTemplatingMapper.java new file mode 100644 index 0000000..08fad5b --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalMultiTemplatingMapper.java @@ -0,0 +1,31 @@ +package de.cuioss.portal.ui.api.templating; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Qualifier; + +/** + * Marker for the portal provided default implementation of + * {@link MultiTemplatingMapper}. Used for injecting or overriding the portals + * defaults implementation. It is @ApplicationScoped and + * + * @Named(PortalCoreBeanNames.MULTI_TEMPLATING_MAPPER_BEAN_NAME) + * + * @author Oliver + * Wolff + */ +@Qualifier +@ApplicationScoped +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalMultiTemplatingMapper { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalMultiViewMapper.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalMultiViewMapper.java new file mode 100644 index 0000000..6a7b30e --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalMultiViewMapper.java @@ -0,0 +1,28 @@ +package de.cuioss.portal.ui.api.templating; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Qualifier; + +/** + * Marker for the portal provided default implementation of + * {@link MultiViewMapper}. Used for injecting or overriding the portals + * defaults implementation. It is @ApplicationScoped and + * + * @Named(MultiViewMapper.BEAN_NAME) @author Oliver Wolff + */ +@Qualifier +@ApplicationScoped +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalMultiViewMapper { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalTemplateDescriptor.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalTemplateDescriptor.java new file mode 100644 index 0000000..4d0d4cb --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalTemplateDescriptor.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.templating; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying concrete instances of {@link StaticTemplateDescriptor}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalTemplateDescriptor { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewDescriptor.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewDescriptor.java new file mode 100644 index 0000000..0f053b1 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewDescriptor.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.templating; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying concrete instances of {@link StaticViewDescriptor}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalViewDescriptor { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewResourcesConfigChanged.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewResourcesConfigChanged.java new file mode 100644 index 0000000..9f6c043 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewResourcesConfigChanged.java @@ -0,0 +1,23 @@ +package de.cuioss.portal.ui.api.templating; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * @author Matthias Walliczek + * + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalViewResourcesConfigChanged { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewResourcesConfigChangedType.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewResourcesConfigChangedType.java new file mode 100644 index 0000000..0e65f58 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/PortalViewResourcesConfigChangedType.java @@ -0,0 +1,15 @@ +package de.cuioss.portal.ui.api.templating; + +/** + * Used for the customization sub-system. Indicates which view-resources have + * been updated. + */ +public enum PortalViewResourcesConfigChangedType { + + /** Indicates view-templates. */ + TEMPLATES, + + /** Indicates actual views. */ + VIEWS + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/StaticTemplateDescriptor.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/StaticTemplateDescriptor.java new file mode 100644 index 0000000..123ee14 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/StaticTemplateDescriptor.java @@ -0,0 +1,27 @@ +package de.cuioss.portal.ui.api.templating; + +import java.io.Serializable; +import java.util.List; + +/** + * Utilized for statically extending the default {@link MultiTemplatingMapper} + * defined by cui-portal-core-cdi-impl. Provides information which template are + * to be handled by which concrete Template-Directory, see package-info for + * details. + * + * @author Oliver Wolff + */ +public interface StaticTemplateDescriptor extends Serializable { + + /** + * @return a List of names of the templates to be handles by this concrete + * descriptor. + */ + List getHandledTemplates(); + + /** + * @return the name of the Template-Directory the templates within this + * descriptor belong to. It must not end with '/' + */ + String getTemplatePath(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/StaticViewDescriptor.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/StaticViewDescriptor.java new file mode 100644 index 0000000..ca9ac39 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/StaticViewDescriptor.java @@ -0,0 +1,26 @@ +package de.cuioss.portal.ui.api.templating; + +import java.io.Serializable; +import java.util.List; + +/** + * Utilized for statically extending the default {@link MultiViewMapper} defined + * by cui-portal-core-cdi-impl. Provides information which views are to be + * handled by which concrete faces-Directory, see package-info for details. + * + * @author Oliver Wolff + */ +public interface StaticViewDescriptor extends Serializable { + + /** + * @return a List of names of the templates to be handles by this concrete + * descriptor. + */ + List getHandledViews(); + + /** + * @return the name of the faces-Directory the templates within this descriptor + * belong to. It must not end with '/' + */ + String getViewPath(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/package-info.java new file mode 100644 index 0000000..7cca496 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/templating/package-info.java @@ -0,0 +1,70 @@ +/** + *

+ * Provides structures enabling / extending the portal for (multi-) templating. + *

+ *

Multi-templating

+ *

+ * The idea of multi-templating is to load template (hierarchies) from the + * jars/modules/external folders, using a {@link de.cuioss.tools.io.FileLoader}, + * see {@link de.cuioss.tools.io.FileLoaderUtility}, not by relative path, as + * within classical JSF-applications. The JSF 2.2. introduced 'contracts' do not + * suffice our needs, especially regarding spreading the templates over multiple + * jars. Therefore we created our own approach, that is similar to contracts. + *

+ *

+ * The ability to resolve template paths dynamically allows to configure + * something like technical root template in the cui-portal and/or other + * templates inside of specific modules and / or applications. + *

+ *

Definitions

+ *
    + *
  • Template-Root-Path: All multi-templating artifacts are to be found at + * META-INF/templates or at + * ExternalConfiguration/templates
  • + *
  • Template-Directory: Within Template-Root-Path are the concrete + * sub-directories located, containing the actual templates for that directory. + * e.g. "/META-INF/templates/portal/" for the portal provided templates or + * "/META-INF/templates/referral/" for the referral provided
  • + *
  • TEMPLATES: Are the actual templates located under the concrete + * Template-Directory e.g. "/META-INF/templates/portal/root.xhtml"
  • + *
+ *

Usage

The usage of multi-templating is straight forward. In your + * facelet template-client you address it like + * + *
+ * {@code }
+ * 
+ *

+ * The /templates part is used for our + * {@link de.cuioss.portal.ui.api.application.templating.ViewResourceHandler} to + * intercept the resolution. The second part identifies the concrete template + * without the template-directory being part of the path: "Give me that thingy" + * instead of "Give me the content of that path". This approach let us keep the + * actual source dynamic. The default implementation will resolve them to + * "/META-INF/templates/portal/technical_root.xhtml" + *

Implementation Details

While the + * {@link de.cuioss.portal.ui.api.application.templating.ViewResourceHandler} + * takes care regarding the delivery of the template the actual logic of which + * template to choose is implemented within concrete instances of + * {@link de.cuioss.portal.ui.api.templating.MultiTemplatingMapper} that needs + * to be registered as managed-bean under the key + * {@value de.cuioss.portal.ui.api.PortalCoreBeanNames#MULTI_TEMPLATING_MAPPER_BEAN_NAME} + *

+ *

Overriding Existing templates

+ *

+ * If a concrete web module wants to to overwrite one or more of the templates, + * e.g "root.xhtml" it needs to: + *

    + *
  • Create a corresponding file, e.g. + * "/META-INF/templates/concrete/root.xhtml"
  • + *
  • Provide either your own implementation of + * {@link de.cuioss.portal.ui.api.templating.MultiTemplatingMapper} resolving + * the request path "root.xhtml" to /concrete/root.xhtml
  • + *
  • Or, preferred, configure the default implementation provided by + * the portal accordingly by Registering concrete instances of + * {@link de.cuioss.portal.ui.api.templating.StaticTemplateDescriptor}
  • + *
      + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.templating; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemeConfiguration.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemeConfiguration.java new file mode 100644 index 0000000..62e4518 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemeConfiguration.java @@ -0,0 +1,25 @@ +package de.cuioss.portal.ui.api.theme; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.application.theme.ThemeConfiguration; + +/** + * Defines Instances of {@link ThemeConfiguration} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalThemeConfiguration { +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemeNameProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemeNameProducer.java new file mode 100644 index 0000000..0590c52 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemeNameProducer.java @@ -0,0 +1,25 @@ +package de.cuioss.portal.ui.api.theme; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.application.theme.ThemeNameProducer; + +/** + * Defines Instances of {@link ThemeNameProducer} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalThemeNameProducer { +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemePersistencesService.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemePersistencesService.java new file mode 100644 index 0000000..ddbf48b --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/PortalThemePersistencesService.java @@ -0,0 +1,23 @@ +package de.cuioss.portal.ui.api.theme; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Defines Instances of {@link ThemePersistenceService} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalThemePersistencesService { +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/ThemePersistenceService.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/ThemePersistenceService.java new file mode 100644 index 0000000..a549515 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/ThemePersistenceService.java @@ -0,0 +1,22 @@ +package de.cuioss.portal.ui.api.theme; + +import de.cuioss.jsf.api.application.theme.ThemeConfiguration; +import de.cuioss.jsf.api.application.theme.ThemeNameProducer; + +/** + * Simple Interface defining methods for storing / saving userdefined themes. + * + * @author Oliver Wolff + */ +public interface ThemePersistenceService extends ThemeNameProducer { + + /** + * Saves the theme if a user has selected a new one in the preferences. The + * Service must store it accordingly. + * + * @param newTheme to be set, must be one of + * {@link ThemeConfiguration#getAvailableThemes()}. The + * implementation must ensure this. + */ + void saveTheme(String newTheme); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/package-info.java new file mode 100644 index 0000000..3ef09d3 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/theme/package-info.java @@ -0,0 +1,7 @@ +/** + * Provides theming related identifier. See + * com.icw.ehf.cui.core.api.application.theme on how theming works within cui + * + * @author Oliver Wolff + */ +package de.cuioss.portal.ui.api.theme; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/Jsf23EnablerBean.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/Jsf23EnablerBean.java new file mode 100644 index 0000000..9e4dd9d --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/Jsf23EnablerBean.java @@ -0,0 +1,16 @@ +package de.cuioss.portal.ui.api.ui; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.annotation.FacesConfig; + +/** + * The presence of the @FacesConfig annotation on a managed bean deployed within + * an application enables version specific features. In this case, it enables + * JSF CDI injection and EL resolution using CDI. + * + * @author Sven Haag, Sven Haag + */ +@ApplicationScoped +@FacesConfig +public class Jsf23EnablerBean { +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/ApplicationProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/ApplicationProducer.java new file mode 100644 index 0000000..5070468 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/ApplicationProducer.java @@ -0,0 +1,38 @@ +package de.cuioss.portal.ui.api.ui.context; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.faces.FactoryFinder; +import javax.faces.application.Application; +import javax.faces.application.ApplicationFactory; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; + +/** + * Produces an {@link ApplicationScoped} {@link Application} instance. In order + * to be independent from individual requests it uses the {@link FactoryFinder} + * directly instead of using {@link FacesContext#getApplication()}. + * + * Mojarra uses {@link ExternalContext#getContext()} which returns the + * application environment object instance for the current application as an + * {@link Object}. + * + * However, we are interested in the actual + * de.cuioss.portal.ui.api.application.PortalApplication stored in + * the + * de.cuioss.portal.ui.api.application.factory.PortalApplicationFactory. + * + * Also see the documentation from Mojarras ApplicationFactory. + * + * @author Sven Haag, Sven Haag + */ +@ApplicationScoped +public class ApplicationProducer { + + @Produces + @ApplicationScoped + Application getApplication() { + return ((ApplicationFactory) FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY)).getApplication(); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CuiCurrentView.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CuiCurrentView.java new file mode 100644 index 0000000..3a073e0 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CuiCurrentView.java @@ -0,0 +1,27 @@ +package de.cuioss.portal.ui.api.ui.context; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.faces.event.PhaseId; +import javax.inject.Qualifier; + +import de.cuioss.jsf.api.common.view.ViewDescriptor; + +/** + * Identifier for the current jsf view, the representation is + * {@link ViewDescriptor}. + * + * Caution: The scope is RequestScoped, therefore you must not inject + * this before {@link PhaseId#RESTORE_VIEW} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +public @interface CuiCurrentView { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CuiNavigationHandler.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CuiNavigationHandler.java new file mode 100644 index 0000000..7018ed5 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CuiNavigationHandler.java @@ -0,0 +1,21 @@ +package de.cuioss.portal.ui.api.ui.context; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.faces.application.NavigationHandler; +import javax.inject.Qualifier; + +/** + * Identifier for the {@link NavigationHandler} + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +public @interface CuiNavigationHandler { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CurrentViewProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CurrentViewProducer.java new file mode 100644 index 0000000..9507698 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/CurrentViewProducer.java @@ -0,0 +1,36 @@ +package de.cuioss.portal.ui.api.ui.context; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.faces.context.FacesContext; +import javax.inject.Named; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.jsf.api.common.view.ViewDescriptorImpl; + +/** + * Produces a requestScoped {@link ViewDescriptor} typed with + * {@link CuiCurrentView}. + * + * @author Oliver Wolff + */ +@ApplicationScoped +public class CurrentViewProducer { + + /** + * @return the derived {@link ViewDescriptor} + */ + @Produces + @Named + @CuiCurrentView + @RequestScoped + ViewDescriptor getCurrentView() { + final var context = FacesContext.getCurrentInstance(); + if (null == context) { + return ViewDescriptorImpl.builder().build(); + } + return NavigationUtils.getCurrentView(context); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/NavigationHandlerProducer.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/NavigationHandlerProducer.java new file mode 100644 index 0000000..9cc590f --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/context/NavigationHandlerProducer.java @@ -0,0 +1,31 @@ +package de.cuioss.portal.ui.api.ui.context; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.faces.application.Application; +import javax.faces.application.NavigationHandler; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Produces a applicationScoped {@link NavigationHandler} instance + * + * @author Oliver Wolff + */ +@ApplicationScoped +public class NavigationHandlerProducer { + + @Inject + private Application application; + + /** + * @return the derived {@link NavigationHandler} + */ + @Produces + @CuiNavigationHandler + @ApplicationScoped + @Named + NavigationHandler getNavigationHandler() { + return application.getNavigationHandler(); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/BaseLazyLoadingRequest.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/BaseLazyLoadingRequest.java new file mode 100644 index 0000000..19e3e82 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/BaseLazyLoadingRequest.java @@ -0,0 +1,22 @@ +package de.cuioss.portal.ui.api.ui.lazyloading; + +import javax.inject.Inject; + +import de.cuioss.jsf.api.components.model.lazyloading.LazyLoadingThreadModel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@EqualsAndHashCode +@ToString +public abstract class BaseLazyLoadingRequest implements LazyLoadingRequest { + + @Getter + @Inject + private LazyLoadingThreadModel viewModel; + + @Override + public long getRequestId() { + return viewModel.getRequestId(); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingErrorHandler.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingErrorHandler.java new file mode 100644 index 0000000..c61cbc3 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingErrorHandler.java @@ -0,0 +1,42 @@ +package de.cuioss.portal.ui.api.ui.lazyloading; + +import java.util.logging.Logger; + +import de.cuioss.jsf.api.components.css.ContextState; +import de.cuioss.jsf.api.components.model.resultContent.ErrorController; +import de.cuioss.jsf.api.components.model.resultContent.ResultErrorHandler; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.uimodel.nameprovider.LabeledKey; +import de.cuioss.uimodel.result.ResultDetail; +import de.cuioss.uimodel.result.ResultState; + +/** + * A default implementation of a error handler for a {@link LazyLoadingRequest}. + * Writes a log message if the state != {@link ResultState#VALID} and sets the + * {@link ContextState} for the notification box. If a + * {@link ResultDetail#getDetail()} is set it will be displayed inside a + * notification box. + */ +public class LazyLoadingErrorHandler extends ResultErrorHandler { + + private static final LabeledKey requestErrorKey = new LabeledKey("message.error.request"); + + /** + * Handles an error during request execution, e.g. timeout. + * + * @param cause a cause if present + * @param message the message to log. + * @param errorController an {@link ErrorController} to allow setting a + * notification box or a GlobalFacesMessage. + * @param log a {@link Logger} to log the cause. + */ + public void handleRequestError(Throwable cause, String message, ErrorController errorController, CuiLogger log) { + if (null != cause) { + log.warn(message, cause); + } else { + log.warn(message); + } + errorController.addNotificationBox(requestErrorKey, ContextState.DANGER); + errorController.setRenderContent(false); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingRequest.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingRequest.java new file mode 100644 index 0000000..d9e67ce --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingRequest.java @@ -0,0 +1,49 @@ +package de.cuioss.portal.ui.api.ui.lazyloading; + +import javax.faces.context.FacesContext; + +import de.cuioss.uimodel.result.ResultDetail; +import de.cuioss.uimodel.result.ResultObject; + +/** + * A request to a backend service, which can be run in a separate thread and + * will return a {@link ResultObject}. On success the result will be handled in + * UI context. + * + * @param should be serializable + */ +public interface LazyLoadingRequest { + + /** + * Trigger a backend request. This request will be started during initialization + * of the view and run in a separate thread without UI context. It must not try + * to access any session specific attributes or parameters or session scoped + * beans and must not try to access the {@link FacesContext}. + * + * @return a {@link ResultObject} + */ + ResultObject backendRequest(); + + /** + * Handle the result of the {@link #backendRequest()}. Will be run in UI + * context. Will always be called, independent from the {@link ResultObject} + * + * @param result the result of the operation. + */ + void handleResult(T result); + + /** + * @return A unique identifier to store the request and allow retrieving of the + * result later. + */ + long getRequestId(); + + /** + * @return an instance of a {@link LazyLoadingErrorHandler} to be called if a + * {@link ResultDetail} was provided in the {@link #backendRequest()}. + */ + default LazyLoadingErrorHandler getErrorHandler() { + return new LazyLoadingErrorHandler(); + } + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingViewController.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingViewController.java new file mode 100644 index 0000000..a64db5a --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/lazyloading/LazyLoadingViewController.java @@ -0,0 +1,15 @@ +package de.cuioss.portal.ui.api.ui.lazyloading; + +/** + * A controller to start {@link LazyLoadingRequest} to be run asynchronously. + */ +public interface LazyLoadingViewController { + + /** + * Starts the {@link LazyLoadingRequest#backendRequest()} method of the given + * {@link LazyLoadingRequest}. + * + * @param request The request to start. + */ + void startRequest(LazyLoadingRequest request); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/AboutPage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/AboutPage.java new file mode 100644 index 0000000..8837167 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/AboutPage.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; + +/** + * Specifies the page bean backing the about page. + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface AboutPage extends Serializable { + + /** + * Bean name for looking up instances. + */ + String BEAN_NAME = "aboutPageBean"; + + /** + * The outcome used for navigation to the help page. + */ + String OUTCOME = "about"; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/AccountPage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/AccountPage.java new file mode 100644 index 0000000..44cade6 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/AccountPage.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; + +/** + * Specifies the page bean backing the account page. + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface AccountPage extends Serializable { + + /** + * Bean name for looking up instances. + */ + String BEAN_NAME = "accountPageBean"; + + /** + * The outcome used for navigation to the account page. + */ + String OUTCOME = "account"; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/ErrorPage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/ErrorPage.java new file mode 100644 index 0000000..24fac76 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/ErrorPage.java @@ -0,0 +1,36 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; + +import de.cuioss.portal.core.storage.PortalSessionStorage; +import de.cuioss.portal.ui.api.exceptions.DefaultErrorMessage; + +/** + * Specifies the page bean backing the error page. + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface ErrorPage extends Serializable { + + /** + * Bean name for looking up instances. + */ + String BEAN_NAME = "errorPageBean"; + + /** + * The outcome used for navigation to the error page. + */ + String OUTCOME = "error"; + + /** + * @return the message derived by {@link PortalSessionStorage} + */ + DefaultErrorMessage getMessage(); + + /** + * @return flag indicating whether a message is available / to be displayed + */ + boolean isMessageAvailable(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/HomePage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/HomePage.java new file mode 100644 index 0000000..51d0c58 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/HomePage.java @@ -0,0 +1,20 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; + +/** + * Specifies the virtual home page. Currently it solely defines the String + * outcome. + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface HomePage extends Serializable { + + /** + * The outcome used for navigation to the home/start page. + */ + String OUTCOME = "home"; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPage.java new file mode 100644 index 0000000..601ca72 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPage.java @@ -0,0 +1,87 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; +import java.util.List; + +import javax.enterprise.context.RequestScoped; + +import de.cuioss.jsf.api.application.history.HistoryManager; +import de.cuioss.portal.authentication.model.UserStore; +import de.cuioss.portal.configuration.PortalConfigurationKeys; +import de.cuioss.uimodel.application.LoginCredentials; + +/** + * Specifies the page bean for the form-login. It should usually be + * {@link RequestScoped} in order to be used with non-transient views. + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface LoginPage extends Serializable { + + /** + * Bean name for looking up instances. + */ + String BEAN_NAME = "loginPageBean"; + + /** + * The outcome used for navigation to the login page. + */ + String OUTCOME = "login"; + + /** URL Parameter / Local Storage key name */ + String KEY_USERSTORE = "userstore"; + + /** URL Parameter / Local Storage key name */ + String KEY_USERNAME = "username"; + + /** Local Storage key name */ + String KEY_REMEMBER_ME = "remember_me"; + + /** + * Init-View-Action used for checking whether the user needs to be redirected. + * In the default implementation this is the case if the user is already + * authenticated. See {@link PortalConfigurationKeys#PAGES_LOGIN_ENTER_STRATEGY} + * for details + * + * @return null if no redirect is necessary otherwise corresponding outcome + */ + String initViewAction(); + + /** + * @return the {@link LoginCredentials} to be filled by the form. + */ + LoginCredentials getLoginCredentials(); + + /** + * @return the list of {@link UserStore} top be used. It is meant to be + * displayed within the drop-down labeled "UserStore" + */ + List getAvailableUserStores(); + + /** + * @return boolean indicating whether to display a dropdown for choosing a + * ldapserver, in other words {@link #getAvailableUserStores() + * .getSize()} > 1 + */ + boolean isShouldDisplayUserStoreDropdown(); + + /** + * Executes the login. + * + * @return String outcome, depending on the login-status. On successful login it + * returns "home", in case of not successful login it returns null. + * Caution: Implementations must support deep-linking, saying a + * requested url must be called after successful login. This is usually + * done by using the {@link HistoryManager} + */ + String login(); + + /** + * @return the string representing the component to be focused: either + * "username" or "password" + */ + String getFocusComponent(); + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageClientStorage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageClientStorage.java new file mode 100644 index 0000000..184d2be --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageClientStorage.java @@ -0,0 +1,44 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.util.function.Supplier; + +import de.cuioss.portal.core.storage.ClientStorage; +import de.cuioss.uimodel.application.LoginCredentials; + +/** + * {@linkplain LoginPageClientStorage} is a decorator for + * {@linkplain ClientStorage} which provide shortcut methods for login page + * specific interaction + * + * @author i000576 + */ +public interface LoginPageClientStorage { + + /** + * {@linkplain Supplier} use {@linkplain ClientStorage} to extract + * {@linkplain LoginPage#KEY_USERSTORE}, {@linkplain LoginPage#KEY_USERNAME} and + * {@linkplain LoginPage#KEY_REMEMBER_ME}. The retrieved + * {@linkplain LoginCredentials} could be still empty if no data is available + * but never {@code null} + * + * @return {@linkplain LoginCredentials} + */ + Supplier extractFromClientStorage(); + + /** + * Update local stored login credentials according passed throw parameter + * + * @param loginCredentials {@linkplain LoginCredentials} must not be + * {@code null} + * @throws {@linkplain NullPointerException} if parameter is {@code null} + */ + void updateLocalStored(final LoginCredentials loginCredentials); + + /** + * Retrieve wrapped + * + * @return {@linkplain ClientStorage} + */ + ClientStorage getWrapped(); + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageHistoryManagerProvider.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageHistoryManagerProvider.java new file mode 100644 index 0000000..08fe92d --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageHistoryManagerProvider.java @@ -0,0 +1,41 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.util.Optional; + +import de.cuioss.jsf.api.application.history.HistoryManager; +import de.cuioss.jsf.api.application.navigation.ViewIdentifier; +import de.cuioss.uimodel.application.LoginCredentials; + +/** + * {@linkplain LoginPageHistoryManagerProvider} is a decorator for + * {@linkplain HistoryManager} which provide shortcut methods for login page + * specific interaction + * + * @author i000576 + */ +public interface LoginPageHistoryManagerProvider { + + /** + * Extract userStore and userName from deep link URL + * + * @param userStore used as default value + * @param username used as default value + * @return option for {@linkplain LoginCredentials} extract from url parameter, + * empty option if parameters are missing + */ + Optional extractFromDeepLinkingUrlParameter(final String userStore, final String username); + + /** + * Retrieve current view with cleaned up URL parameter + * + * @return {@linkplain ViewIdentifier} + */ + ViewIdentifier getCurrentViewExcludeUserStoreAndUserName(); + + /** + * Retrieve wrapped Object + * + * @return {@linkplain HistoryManager} + */ + HistoryManager getWrapped(); +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageStrategy.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageStrategy.java new file mode 100644 index 0000000..671be24 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LoginPageStrategy.java @@ -0,0 +1,47 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static de.cuioss.tools.string.MoreStrings.requireNotEmpty; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Oliver Wolff + * + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum LoginPageStrategy { + + /** Defines that the login-page should redirect to home. */ + GOTO_HOME("goto_home"), + + /** + * Defines that the login-page should logout if user is already logged in. + */ + LOGOUT("logout"); + + @Getter + private final String strategyName; + + /** + * Factory method for creating {@link LoginPageStrategy} instances from a given + * String + * + * @param loginPageStrategyName must not be null and represent an instance of + * {@link LoginPageStrategy#values()} + * @return the found {@link LoginPageStrategy} + * + * @throws IllegalArgumentException if no strategy can be found + */ + public static LoginPageStrategy getFromString(final String loginPageStrategyName) { + requireNotEmpty(loginPageStrategyName, "loginPageStrategyName"); + var lowercaseName = loginPageStrategyName.toLowerCase(); + for (LoginPageStrategy strategy : LoginPageStrategy.values()) { + if (strategy.strategyName.equals(lowercaseName)) { + return strategy; + } + } + throw new IllegalArgumentException("No loginPageStrategyName found for " + loginPageStrategyName); + } +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LogoutPage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LogoutPage.java new file mode 100644 index 0000000..5edd674 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/LogoutPage.java @@ -0,0 +1,39 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; + +/** + * Provides methods for handling the logout. It can be used from the logout page + * {@link #logoutViewAction()} or by programmatically calling + * {@link #performLogout()} + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface LogoutPage extends Serializable { + + /** + * Bean name for looking up instances. + */ + String BEAN_NAME = "logoutPageBean"; + + /** + * The outcome used for navigation to the logout page. + */ + String OUTCOME = "logout"; + + /** + * A view action triggering the logout. + * + * @return null in every case, due to signature constraint from jsf-view-action + */ + String logoutViewAction(); + + /** + * @return null if logout was successful otherwise {@link LoginPage#OUTCOME} in + * order to force a login. + */ + String performLogout(); + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesAbout.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesAbout.java new file mode 100644 index 0000000..3daa6cb --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesAbout.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying instances of {@link AboutPage}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalCorePagesAbout { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesAccount.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesAccount.java new file mode 100644 index 0000000..234b2fc --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesAccount.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying instances of {@link AccountPage}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalCorePagesAccount { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesError.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesError.java new file mode 100644 index 0000000..0daf34b --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesError.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying instances of {@link ErrorPage}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalCorePagesError { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesLogin.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesLogin.java new file mode 100644 index 0000000..3afcaa1 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesLogin.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying instances of {@link LoginPage}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalCorePagesLogin { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesLogout.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesLogout.java new file mode 100644 index 0000000..dbeaa89 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesLogout.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying instances of {@link LogoutPage}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalCorePagesLogout { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesPreferences.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesPreferences.java new file mode 100644 index 0000000..f277270 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PortalCorePagesPreferences.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying instances of {@link PreferencesPage}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalCorePagesPreferences { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PreferencesPage.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PreferencesPage.java new file mode 100644 index 0000000..731a897 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/PreferencesPage.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import java.io.Serializable; + +/** + * Specifies the page bean backing the preferences page. + * + * @author Oliver Wolff + */ +@SuppressWarnings("squid:S1214") // We allow constants in the page interfaces, because they belong together + // (coherence). +public interface PreferencesPage extends Serializable { + + /** + * Bean name for looking up instances. + */ + String BEAN_NAME = "preferencesPageBean"; + + /** + * The outcome used for navigation to the preferences page. + */ + String OUTCOME = "preferences"; + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/package-info.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/package-info.java new file mode 100644 index 0000000..df53e99 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/ui/pages/package-info.java @@ -0,0 +1,7 @@ +/** + * Provides interfaces describing the pages provided by the portal. For each + * page there is an Interface and a corresponding annotation, e.g. + * {@link de.cuioss.portal.ui.api.ui.pages.LoginPage} and + * {@link de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogin} + */ +package de.cuioss.portal.ui.api.ui.pages; diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/view/PortalViewRestrictionManager.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/view/PortalViewRestrictionManager.java new file mode 100644 index 0000000..415a912 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/view/PortalViewRestrictionManager.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.view; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Marker identifying concrete instances of {@link ViewRestrictionManager}. + * + * @author Oliver Wolff + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalViewRestrictionManager { + +} diff --git a/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/view/ViewRestrictionManager.java b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/view/ViewRestrictionManager.java new file mode 100644 index 0000000..e244c69 --- /dev/null +++ b/modules/portal-ui-api/src/main/java/de/cuioss/portal/ui/api/view/ViewRestrictionManager.java @@ -0,0 +1,52 @@ +package de.cuioss.portal.ui.api.view; + +import java.io.Serializable; +import java.util.Set; + +import javax.enterprise.context.SessionScoped; + +import de.cuioss.jsf.api.common.view.ViewDescriptor; + +/** + * The ViewRestrictionManager is used for deciding whether a concrete view is + * authorized for the current users. The implementation are therefore stateful, + * usually {@link SessionScoped}. + * + * @author Oliver Wolff + */ +public interface ViewRestrictionManager extends Serializable { + + /** + * Determines the roles required for accessing this specific views. + * + * @param descriptor identifying the view to be accessed, must not be null + * @return an immutable {@link Set} of role-names needed to access this views, + * may be empty but never {@code null} + */ + Set getRequiredRolesForView(ViewDescriptor descriptor); + + /** + * Determines whether the currently logged in user is allowed / authorized to + * access the given view. + * + * @param descriptor identifying the view to be accessed, must not be null + * @return a boolean indicating whether the current user is authorized to access + * the given view {@code true} or not {@code false} + */ + boolean isUserAuthorized(ViewDescriptor descriptor); + + /** + * Determines whether the currently logged in user is allowed / authorized to + * access the given view, identified by the given outcome. + * + * @param viewOutcome String outcome identifying a concrete view that should be + * checked + * @return a boolean indicating whether the current user is authorized to access + * the given view {@code true} or not {@code false} + * @throws IllegalStateException signaling, that the view can not not + * determined, e.g.g there is no navigation-rule + * defined for the given outcome + */ + boolean isUserAuthorizedForViewOutcome(String viewOutcome); + +} diff --git a/modules/portal-ui-api/src/main/resources/META-INF/beans.xml b/modules/portal-ui-api/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..d54a394 --- /dev/null +++ b/modules/portal-ui-api/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/ModuleConsistencyTest.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/ModuleConsistencyTest.java new file mode 100644 index 0000000..3b62336 --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/ModuleConsistencyTest.java @@ -0,0 +1,18 @@ +package de.cuioss.portal.ui.api; + +import org.jboss.weld.environment.se.Weld; + +import de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock; +import de.cuioss.portal.core.test.tests.BaseModuleConsistencyTest; +import de.cuioss.portal.ui.api.test.support.PortalResourceBundleMock; +import de.cuioss.test.jsf.producer.JsfObjectsProducers; +import de.cuioss.test.jsf.producer.ServletObjectsFromJSFContextProducers; + +class ModuleConsistencyTest extends BaseModuleConsistencyTest { + + @Override + protected Weld modifyWeldContainer(Weld weld) { + return weld.addBeanClasses(ServletObjectsFromJSFContextProducers.class, JsfObjectsProducers.class, + PortalAuthenticationFacadeMock.class, PortalResourceBundleMock.class); + } +} diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/StickyMessageTest.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/StickyMessageTest.java new file mode 100644 index 0000000..eabf6c1 --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/StickyMessageTest.java @@ -0,0 +1,14 @@ +package de.cuioss.portal.ui.api; + +import de.cuioss.portal.ui.api.message.StickyMessage; +import de.cuioss.portal.ui.api.test.support.IDisplayNameProviderTypedGenerator; +import de.cuioss.test.valueobjects.ValueObjectTest; +import de.cuioss.test.valueobjects.api.contracts.VerifyBuilder; +import de.cuioss.test.valueobjects.api.generator.PropertyGenerator; + +@SuppressWarnings("javadoc") +@PropertyGenerator(IDisplayNameProviderTypedGenerator.class) +@VerifyBuilder(required = { "state", "message" }) +class StickyMessageTest extends ValueObjectTest { + +} diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/menu/MockPortalNavigationMenuItemImplBase.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/menu/MockPortalNavigationMenuItemImplBase.java new file mode 100644 index 0000000..c4eab43 --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/menu/MockPortalNavigationMenuItemImplBase.java @@ -0,0 +1,15 @@ +package de.cuioss.portal.ui.api.menu; + +import javax.enterprise.context.Dependent; + +@Dependent +public class MockPortalNavigationMenuItemImplBase extends PortalNavigationMenuItemImplBase { + + public static final String ID = "mock"; + private static final long serialVersionUID = 4979985201723205870L; + + @Override + public String getId() { + return ID; + } +} diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemImplBaseTest.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemImplBaseTest.java new file mode 100644 index 0000000..39ae40c --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/menu/PortalNavigationMenuItemImplBaseTest.java @@ -0,0 +1,71 @@ +package de.cuioss.portal.ui.api.menu; + +import static de.cuioss.portal.configuration.PortalConfigurationKeys.MENU_BASE; +import static de.cuioss.portal.configuration.PortalConfigurationKeys.MENU_TOP_IDENTIFIER; +import static de.cuioss.portal.ui.api.menu.MockPortalNavigationMenuItemImplBase.ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.configuration.PortalConfigurationSource; +import de.cuioss.portal.core.test.junit5.EnablePortalConfiguration; +import de.cuioss.portal.core.test.mocks.configuration.PortalTestConfiguration; +import de.cuioss.test.jsf.junit5.EnableJsfEnvironment; +import de.cuioss.test.jsf.producer.JsfObjectsProducers; +import lombok.Getter; + +@EnableAutoWeld +@AddBeanClasses({ JsfObjectsProducers.class }) +@EnableJsfEnvironment +@EnablePortalConfiguration +class PortalNavigationMenuItemImplBaseTest { + + private static final String BASE = MENU_BASE + ID + "."; + + private static final String ENABLED = BASE + "enabled"; + private static final String ORDER = BASE + "order"; + private static final String PARENT = BASE + "parent"; + + private static final Integer DEFAULT_ORDER = 10; + private static final String DEFAULT_PARENT = MENU_TOP_IDENTIFIER; + + @Inject + @Getter + private MockPortalNavigationMenuItemImplBase underTest; + + @Inject + private Provider underTestProvider; + + @Inject + @PortalConfigurationSource + private PortalTestConfiguration configuration; + + @Test + void shouldHandleMissingConfigurationGracefully() { + assertNull(underTest.getParentId()); + assertEquals(Integer.valueOf(-1), underTest.getOrder()); + assertNull(underTest.getIconStyleClass()); + assertNull(underTest.getResolvedLabel()); + assertNull(underTest.getResolvedTitle()); + assertFalse(underTest.isRendered()); + } + + @Test + void shouldHandleConfiguration() { + configuration.fireEvent(ENABLED, "true", ORDER, DEFAULT_ORDER.toString()); + configuration.fireEvent(PARENT, DEFAULT_PARENT); + + underTest = underTestProvider.get(); + assertEquals(DEFAULT_PARENT, underTest.getParentId()); + assertEquals(DEFAULT_ORDER, underTest.getOrder()); + assertTrue(underTest.isRendered()); + } +} diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/test/support/IDisplayNameProviderTypedGenerator.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/test/support/IDisplayNameProviderTypedGenerator.java new file mode 100644 index 0000000..2edffd7 --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/test/support/IDisplayNameProviderTypedGenerator.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.api.test.support; + +import static de.cuioss.test.generator.Generators.nonEmptyStrings; + +import de.cuioss.test.generator.TypedGenerator; +import de.cuioss.uimodel.nameprovider.DisplayName; +import de.cuioss.uimodel.nameprovider.IDisplayNameProvider; + +/** + * Typed generator for {@link IDisplayNameProvider} + */ +@SuppressWarnings("rawtypes") +public class IDisplayNameProviderTypedGenerator implements TypedGenerator { + + @Override + public Class getType() { + return IDisplayNameProvider.class; + } + + @Override + public IDisplayNameProvider next() { + return new DisplayName(nonEmptyStrings().next()); + } +} diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/test/support/PortalResourceBundleMock.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/test/support/PortalResourceBundleMock.java new file mode 100644 index 0000000..36f6f76 --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/test/support/PortalResourceBundleMock.java @@ -0,0 +1,42 @@ +package de.cuioss.portal.ui.api.test.support; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.ResourceBundle; + +import javax.enterprise.context.Dependent; +import javax.inject.Named; + +import de.cuioss.jsf.api.application.bundle.CuiResourceBundle; +import de.cuioss.portal.core.bundle.PortalResourceBundle; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Mock variant of {@link CuiResourceBundle}. Simulate + * {@link #getString(String)} (={@link #getObject(String)}) by simply returning + * the key (like PortalMessageProducerMock ). {@link #getKeys()} will return an + * empty list. + * + * @author Oliver Wolff + */ +@Named("msgs") +@PortalResourceBundle +@Dependent +@EqualsAndHashCode(callSuper = false) +@ToString +public class PortalResourceBundleMock extends ResourceBundle implements Serializable { + + private static final long serialVersionUID = 3953649686127640297L; + + @Override + protected Object handleGetObject(final String key) { + return key; + } + + @Override + public Enumeration getKeys() { + return Collections.emptyEnumeration(); + } +} diff --git a/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/ui/pages/LoginPageStrategyTest.java b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/ui/pages/LoginPageStrategyTest.java new file mode 100644 index 0000000..5f569b9 --- /dev/null +++ b/modules/portal-ui-api/src/test/java/de/cuioss/portal/ui/api/ui/pages/LoginPageStrategyTest.java @@ -0,0 +1,30 @@ +package de.cuioss.portal.ui.api.ui.pages; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class LoginPageStrategyTest { + + @Test + void shouldReturnStrategyOnExisitingName() { + assertEquals(LoginPageStrategy.GOTO_HOME, + LoginPageStrategy.getFromString(LoginPageStrategy.GOTO_HOME.getStrategyName())); + assertEquals(LoginPageStrategy.LOGOUT, + LoginPageStrategy.getFromString(LoginPageStrategy.LOGOUT.getStrategyName())); + + assertEquals(LoginPageStrategy.GOTO_HOME, + LoginPageStrategy.getFromString(LoginPageStrategy.GOTO_HOME.getStrategyName().toUpperCase())); + assertEquals(LoginPageStrategy.LOGOUT, + LoginPageStrategy.getFromString(LoginPageStrategy.LOGOUT.getStrategyName().toUpperCase())); + } + + @Test + void shouldFailOnInvalidName() { + assertThrows(IllegalArgumentException.class, () -> { + LoginPageStrategy.getFromString("NoThere"); + }); + } + +} diff --git a/modules/portal-ui-bootstrap-page-templates/README.asciidoc b/modules/portal-ui-bootstrap-page-templates/README.asciidoc new file mode 100644 index 0000000..b1aa108 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/README.asciidoc @@ -0,0 +1,28 @@ += portal-ui-bootstrap-page-templates + +== What is it? + +Provides Facelet templates for creating portal pages. It is based on Twitters bootstrap + +In addition it provides some JSF-Components for the layouts (previous portal-ui-components) + +== Maven Coordinates + +[source,xml] +---- + + de.cuioss.portal.ui + portal-ui-bootstrap-page-templates + +---- + +== Info + +Downstream modules like like + +* portal-ui-form-based-login +* portal-ui-components + +rely on these templates. + +So if you want / need to replace them, you have to consider them as well \ No newline at end of file diff --git a/modules/portal-ui-bootstrap-page-templates/pom.xml b/modules/portal-ui-bootstrap-page-templates/pom.xml new file mode 100644 index 0000000..fa1f03e --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + de.cuioss.portal.ui + modules + 1.0.0-SNAPSHOT + + portal-ui-bootstrap-page-templates + Portal UI Bootstrap Page Templates + Provides Facelet templates for creating portal pages. It is + based on Twitters bootstrap + + + de.cuioss.portal.ui + portal-ui-api + + + + de.cuioss.jsf + cui-jsf-api + + + de.cuioss.jsf + cui-jsf-bootstrap + compile + + + + de.cuioss.portal.ui + portal-ui-unit-testing + + + \ No newline at end of file diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/PortalCssClasses.java b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/PortalCssClasses.java new file mode 100644 index 0000000..5b1aefa --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/PortalCssClasses.java @@ -0,0 +1,28 @@ +package de.cuioss.portal.ui.components; + +import de.cuioss.jsf.api.components.css.StyleClassBuilder; +import de.cuioss.jsf.api.components.css.StyleClassProvider; +import de.cuioss.jsf.api.components.css.impl.StyleClassBuilderImpl; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Provides the css-classes + * + * @author Oliver Wolff + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum PortalCssClasses implements StyleClassProvider { + + /** "sidebar" */ + SIDEBAR("sidebar"); + + @Getter + private final String styleClass; + + @Override + public StyleClassBuilder getStyleClassBuilder() { + return new StyleClassBuilderImpl(styleClass); + } +} diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/PortalFamily.java b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/PortalFamily.java new file mode 100644 index 0000000..fdde18b --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/PortalFamily.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.components; + +import de.cuioss.jsf.bootstrap.accordion.AccordionComponent; +import de.cuioss.portal.ui.components.layout.SidebarComponent; +import lombok.experimental.UtilityClass; + +/** + * Simple Container for identifying portal-components family + * + * @author Oliver Wolff + */ +@UtilityClass +public final class PortalFamily { + + /** Defines the portal components family. */ + public static final String PORTAL_FAMILY = "de.icw.cui.portal.family"; + + /** The component for {@link SidebarComponent} */ + public static final String SIDEBAR_COMPONENT = "de.icw.cui.portal.sidebar"; + + /** Default Renderer for {@link AccordionComponent} */ + public static final String SIDEBAR_RENDERER = "de.icw.cui.portal.sidebar_renderer"; + +} diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/layout/SidebarComponent.java b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/layout/SidebarComponent.java new file mode 100644 index 0000000..08918a2 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/layout/SidebarComponent.java @@ -0,0 +1,92 @@ +package de.cuioss.portal.ui.components.layout; + +import javax.faces.component.FacesComponent; +import javax.faces.context.FacesContext; + +import de.cuioss.jsf.api.components.base.BaseCuiPanel; +import de.cuioss.jsf.api.components.css.StyleClassBuilder; +import de.cuioss.jsf.api.components.css.StyleClassResolver; +import de.cuioss.jsf.api.components.html.Node; +import de.cuioss.jsf.api.components.partial.HtmlElementProvider; +import de.cuioss.jsf.api.components.util.ComponentUtility; +import de.cuioss.portal.ui.components.PortalCssClasses; +import de.cuioss.portal.ui.components.PortalFamily; +import lombok.experimental.Delegate; + +/** + * Renders a container for the sidebar within portal-context. Actually it + * renders a div / nav with the style-class {@link PortalCssClasses#SIDEBAR}. + * The corresponding elements are defined at the root.xhtml template + * + *

      Sidebar Left Sample

      + * + *
      + * {@code
      +    
      +        
      +            

      Some Content in the sidebar

      +
      +
      } + *
      + * + *

      Sidebar Right Sample

      + * + *
      + * {@code
      +    
      +        
      +            

      Some Content in the right sidebar

      +
      +
      } + *
      + * + *

      Attributes

      + *
        + *
      • Common attributes like style, styleClass, rendered and id
      • + *
      • {@link HtmlElementProvider}, defaulting to {@value Node#NAV}
      • + *
      + *

      Styling

      + *
        + *
      • The marker css class is '{@value PortalCssClasses#SIDEBAR}'
      • + *
      + * + * @author Oliver Wolff + * + */ +@FacesComponent(PortalFamily.SIDEBAR_COMPONENT) +@SuppressWarnings("squid:MaximumInheritanceDepth") // Artifact of Jsf-structure +public class SidebarComponent extends BaseCuiPanel implements StyleClassResolver { + + @Delegate + private final HtmlElementProvider htmlElementProvider; + + /** + * Default Constructor + */ + public SidebarComponent() { + htmlElementProvider = new HtmlElementProvider(this, Node.NAV); + super.setRendererType(PortalFamily.SIDEBAR_RENDERER); + } + + @Override + public StyleClassBuilder resolveStyleClass() { + return PortalCssClasses.SIDEBAR.getStyleClassBuilder().append(super.getStyleClass()); + } + + @Override + public String getFamily() { + return PortalFamily.PORTAL_FAMILY; + } + + /** + * Shortcut for creating and casting a component of type + * {@link SidebarComponent}. + * + * @param facesContext + * @return a newly created {@link SidebarComponent} + */ + public static SidebarComponent createComponent(final FacesContext facesContext) { + return ComponentUtility.createComponent(facesContext, PortalFamily.SIDEBAR_COMPONENT); + } + +} diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/layout/SidebarComponentRenderer.java b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/layout/SidebarComponentRenderer.java new file mode 100644 index 0000000..a97f1d2 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/java/de/cuioss/portal/ui/components/layout/SidebarComponentRenderer.java @@ -0,0 +1,42 @@ +package de.cuioss.portal.ui.components.layout; + +import java.io.IOException; + +import javax.faces.context.FacesContext; +import javax.faces.render.FacesRenderer; +import javax.faces.render.Renderer; + +import de.cuioss.jsf.api.components.renderer.BaseDecoratorRenderer; +import de.cuioss.jsf.api.components.renderer.DecoratingResponseWriter; +import de.cuioss.portal.ui.components.PortalFamily; + +/** + * {@link Renderer} for {@link SidebarComponent} + * + * @author Oliver Wolff + */ +@FacesRenderer(rendererType = PortalFamily.SIDEBAR_RENDERER, componentFamily = PortalFamily.PORTAL_FAMILY) +public class SidebarComponentRenderer extends BaseDecoratorRenderer { + + /** + */ + public SidebarComponentRenderer() { + super(false); + } + + @Override + protected void doEncodeBegin(final FacesContext context, final DecoratingResponseWriter writer, + final SidebarComponent component) throws IOException { + writer.withStartElement(component.resolveHtmlElement()); + writer.withStyleClass(component.resolveStyleClass()); + writer.withAttributeStyle(component.getStyle()); + writer.withClientIdIfNecessary(); + writer.withPassThroughAttributes(); + } + + @Override + protected void doEncodeEnd(final FacesContext context, final DecoratingResponseWriter writer, + final SidebarComponent component) throws IOException { + writer.withEndElement(component.resolveHtmlElement()); + } +} diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/beans.xml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..d54a394 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces-config.xml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces-config.xml new file mode 100644 index 0000000..88ec65f --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces-config.xml @@ -0,0 +1,7 @@ + + + CuiPortalUiComponents + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/about.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/about.xhtml new file mode 100644 index 0000000..db640e1 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/about.xhtml @@ -0,0 +1,61 @@ + + + + + #{msgs['page.about.title']} + + + +
      +
      +
      +
      +

      #{msgs['portal.solution.title']}

      +

      + + +

      +

      + #{msgs['page.about.release.prefix']} + #{msgs['appVersion']} + +

      +

      #{msgs['page.about.vendor.info']}

      +

      #{msgs['page.about.company.name']}

      +
      + +
      + +
      + +
      + +
      #{msgs['page.about.company.email']}
      + #{msgs['page.about.company.webpage']} +
      + +
      +
      + +
      +
      +
      +
      +
      +
      + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/account.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/account.xhtml new file mode 100644 index 0000000..9f756e6 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/account.xhtml @@ -0,0 +1,10 @@ + + + + #{msgs['page.account.title']} + +

      #{msgs['page.account.srHeader']}

      +
      +
      + \ No newline at end of file diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/preferences.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/preferences.xhtml new file mode 100644 index 0000000..16b4924 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/account/preferences.xhtml @@ -0,0 +1,52 @@ + + + + #{msgs['page.preferences.title']} + + +

      #{msgs['page.preferences.srHeader']}

      + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/index.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/index.xhtml new file mode 100644 index 0000000..9f9c747 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/faces/pages/index.xhtml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/portal.taglib.xml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/portal.taglib.xml new file mode 100644 index 0000000..e051947 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/portal.taglib.xml @@ -0,0 +1,158 @@ + + + ttps://cuioss.de/jsf/portal + portal + + + + Renders a container for the sidebar within portal-context. Actually it renders a div / nav with the style-class 'sidebar'. + The corresponding elements are defined at the root.xhtml template. +

      +

      Sidebar Left Sample

      +
      +            
      +                
      +                    

      Some Content in the sidebar

      +
      +
      +
      +

      Sidebar Right Sample

      +
      +            
      +                
      +                    

      Some Content in the right sidebar

      +
      +
      +
      + +

      Styling

      +
        +
      • The marker css class is 'sidebar'
      • +
      + ]]> +
      + sidebar + + de.icw.cui.portal.sidebar + de.icw.cui.portal.sidebar_renderer + + + + + id + false + java.lang.String + + + + + rendered + false + boolean + + + + + + htmlElement + false + java.lang.String + + + + + + styleClass + false + java.lang.String + + + + + + style + false + java.lang.String + +
      + + Summary +

      Renders a list of sticky messages

      +

      Assumptions

      +
        +
      • An instance of de.cuioss.portal.ui.api.message.StickyMessageProducer being present under the name 'stickyMessageProducer'
      • +
      • Available Primefaces
      • +
      +

      Behavior

      +

      Renders an auto-updatable primefaces output-panel with a form containing the messages to be displayed

      + ]]>
      + stickyMessages + + portal-ui-components/stickyMessages.xhtml + + + + + id + false + java.lang.String + + + + + rendered + false + boolean + +
      + + Summary +

      Renders a navigation menu. Supports header- and footer-facet Caution: Will soon be replaced

      + ]]>
      + navigationMenu + + portal-ui-components/navigationMenu.xhtml + + + + + id + false + java.lang.String + + + + + rendered + false + boolean + + + + + modelItems + true + java.util.List + +
      +
      diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/de.icw.cui.portal/about_page.css b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/de.icw.cui.portal/about_page.css new file mode 100644 index 0000000..13e1191 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/de.icw.cui.portal/about_page.css @@ -0,0 +1,26 @@ +/* hide empty element */ + +.about-content span:empty { + display: none; +} + +.about-content p:empty { + display: none; +} + +.about-content div:empty { + display: none; +} + +.company-phone-number, +.company-phone-number + br, +.company-email, +.company-email + br { + display: none; +} + +.solution-title { + margin-right: 3px; +} + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/portal-ui-components/navigationMenu.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/portal-ui-components/navigationMenu.xhtml new file mode 100644 index 0000000..65ef66a --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/portal-ui-components/navigationMenu.xhtml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/portal-ui-components/stickyMessages.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/portal-ui-components/stickyMessages.xhtml new file mode 100644 index 0000000..a44cfe4 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/resources/portal-ui-components/stickyMessages.xhtml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/http_error_page.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/http_error_page.xhtml new file mode 100644 index 0000000..08c4580 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/http_error_page.xhtml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/layout_footer.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/layout_footer.xhtml new file mode 100644 index 0000000..83ee723 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/layout_footer.xhtml @@ -0,0 +1,18 @@ + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/master.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/master.xhtml new file mode 100644 index 0000000..bd6ec48 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/master.xhtml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/master_centered.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/master_centered.xhtml new file mode 100644 index 0000000..370da19 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/master_centered.xhtml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/plainView.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/plainView.xhtml new file mode 100644 index 0000000..9c2ff8f --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/plainView.xhtml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/root.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/root.xhtml new file mode 100644 index 0000000..cbf68ba --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/root.xhtml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/technical_root.xhtml b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/technical_root.xhtml new file mode 100644 index 0000000..073b820 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/main/resources/META-INF/templates/portal/technical_root.xhtml @@ -0,0 +1,155 @@ + + + + + + + + + + <ui:insert name="title"> + <h:outputText value="#{msgs['portal.title']}" /> + </ui:insert> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/ModuleConsistencyTest.java b/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/ModuleConsistencyTest.java new file mode 100644 index 0000000..feb6934 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/ModuleConsistencyTest.java @@ -0,0 +1,18 @@ +package de.cuioss.portal.ui.components; + +import org.junit.jupiter.api.Disabled; + +import de.cuioss.portal.core.test.tests.BaseModuleConsistencyTest; + +/** + * Tests the complete cdi environment / wiring + * + * @author Oliver Wolff + */ +class ModuleConsistencyTest extends BaseModuleConsistencyTest { + + @Override + @Disabled("Currently there is the need to portal-core-impl modules. This needs to be fixed: PortalHistoryManager") + protected void shouldStartUpContainer() { + } +} diff --git a/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/layout/SidebarComponentRendererTest.java b/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/layout/SidebarComponentRendererTest.java new file mode 100644 index 0000000..96e84c7 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/layout/SidebarComponentRendererTest.java @@ -0,0 +1,36 @@ +package de.cuioss.portal.ui.components.layout; + +import javax.faces.component.UIComponent; +import javax.faces.component.html.HtmlOutputText; + +import org.junit.jupiter.api.Test; + +import de.cuioss.jsf.api.components.html.HtmlTreeBuilder; +import de.cuioss.jsf.api.components.html.Node; +import de.cuioss.portal.ui.components.PortalCssClasses; +import de.cuioss.test.jsf.renderer.AbstractComponentRendererTest; + +class SidebarComponentRendererTest extends AbstractComponentRendererTest { + + @Test + void shouldRenderMinimal() { + var component = new SidebarComponent(); + var expected = new HtmlTreeBuilder().withNode(Node.NAV).withStyleClass(PortalCssClasses.SIDEBAR); + assertRenderResult(component, expected.getDocument()); + } + + @Test + void shouldRenderWithChildren() { + var component = new SidebarComponent(); + component.getChildren().add(new HtmlOutputText()); + getComponentConfigDecorator().registerMockRendererForHtmlOutputText(); + var expected = new HtmlTreeBuilder().withNode(Node.NAV).withStyleClass(PortalCssClasses.SIDEBAR) + .withNode("HtmlOutputText"); + assertRenderResult(component, expected.getDocument()); + } + + @Override + protected UIComponent getComponent() { + return new SidebarComponent(); + } +} diff --git a/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/layout/SidebarComponentTest.java b/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/layout/SidebarComponentTest.java new file mode 100644 index 0000000..5d6ee05 --- /dev/null +++ b/modules/portal-ui-bootstrap-page-templates/src/test/java/de/cuioss/portal/ui/components/layout/SidebarComponentTest.java @@ -0,0 +1,30 @@ +package de.cuioss.portal.ui.components.layout; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import de.cuioss.jsf.api.components.html.Node; +import de.cuioss.portal.ui.components.PortalCssClasses; +import de.cuioss.portal.ui.components.PortalFamily; +import de.cuioss.test.jsf.component.AbstractUiComponentTest; + +class SidebarComponentTest extends AbstractUiComponentTest { + + @Test + void shouldProvideCorrectStyleClass() { + assertEquals(PortalCssClasses.SIDEBAR.getStyleClass(), anyComponent().resolveStyleClass().getStyleClass()); + } + + @Test + void shouldDefaultToNav() { + assertEquals(Node.NAV, anyComponent().resolveHtmlElement()); + } + + @Test + void shouldProvideCorrectMetadata() { + assertEquals(PortalFamily.PORTAL_FAMILY, anyComponent().getFamily()); + assertEquals(PortalFamily.SIDEBAR_RENDERER, anyComponent().getRendererType()); + } + +} diff --git a/modules/portal-ui-errorpages/pom.xml b/modules/portal-ui-errorpages/pom.xml new file mode 100644 index 0000000..fa185da --- /dev/null +++ b/modules/portal-ui-errorpages/pom.xml @@ -0,0 +1,38 @@ + + 4.0.0 + + de.cuioss.portal.ui + modules + 1.0.0-SNAPSHOT + + portal-ui-errorpages + Portal UI Error-Pages + Provides some default error-pages + + + de.cuioss.portal.ui + portal-ui-api + + + de.cuioss.portal.ui + portal-ui-runtime + + + + de.cuioss.jsf + cui-jsf-api + + + + + de.cuioss.portal.ui + portal-ui-unit-testing + + + de.cuioss.portal.authentication + portal-authentication-mock + + + \ No newline at end of file diff --git a/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/AbstractHttpErrorPage.java b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/AbstractHttpErrorPage.java new file mode 100644 index 0000000..e17767b --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/AbstractHttpErrorPage.java @@ -0,0 +1,99 @@ +package de.icw.cui.portal.ui.errorpages; + +import static de.cuioss.tools.string.MoreStrings.isEmpty; + +import java.io.Serializable; + +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.tools.logging.CuiLogger; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * @author Oliver Wolff + * + */ +@EqualsAndHashCode +@ToString(exclude = { "facesContextProvider" }) +public abstract class AbstractHttpErrorPage implements Serializable { + + private static final CuiLogger log = new CuiLogger(AbstractHttpErrorPage.class); + + private static final String UNKNOWN = "?"; + static final String JAVAX_SERVLET_ERROR_REQUEST_URI = "javax.servlet.error.request_uri"; + + private static final long serialVersionUID = -6617663225820801072L; + + @Inject + @Getter(AccessLevel.PROTECTED) + private Provider facesContextProvider; + + @Getter + private String requestUri; + + /** boolean indicating whether the requested view is jsf view or not. */ + @Getter + private boolean jsfView; + + /** + * Initializes the view by determining the requestedUri and logging the + * errorCode at warn-level + * + * @return always {@code null} + */ + public String initView() { + var context = facesContextProvider.get(); + var request = ServletAdapterUtil.getRequest(context); + requestUri = determineRequestUri(request); + jsfView = requestUri.startsWith(NavigationUtils.FACES_VIEW_PREFIX); + ServletAdapterUtil.getResponse(context).setStatus(getErrorCode()); + log.warn("Portal-137: Http-Error '{}' for requested-uri '{}' was raised, jsfView='{}'", getErrorCode(), + requestUri, jsfView); + return null; + } + + /** + * @return boolean indicating whether the requestUri could be determined. + */ + public boolean isRequestUriAvailable() { + return !UNKNOWN.equals(requestUri); + } + + /** + * @param request + * + * @return + */ + private String determineRequestUri(HttpServletRequest request) { + if (null == request) { + return UNKNOWN; + } + var attribute = request.getAttribute(JAVAX_SERVLET_ERROR_REQUEST_URI); + if (null == attribute) { + return UNKNOWN; + } + var aString = String.valueOf(attribute); + if (isEmpty(aString)) { + return UNKNOWN; + } + var context = request.getContextPath(); + if (aString.startsWith(context)) { + return aString.substring(context.length()); + } + return aString; + } + + /** + * @return the concrete error code. It is usually defined by the concrete view / + * backing bean + */ + protected abstract int getErrorCode(); +} diff --git a/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/ErrorPageBean.java b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/ErrorPageBean.java new file mode 100644 index 0000000..65c3a40 --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/ErrorPageBean.java @@ -0,0 +1,70 @@ +package de.icw.cui.portal.ui.errorpages; + +import java.io.Serializable; + +import javax.annotation.PostConstruct; +import javax.annotation.Priority; +import javax.enterprise.context.RequestScoped; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletResponse; + +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.portal.configuration.common.PortalPriorities; +import de.cuioss.portal.core.storage.MapStorage; +import de.cuioss.portal.core.storage.PortalSessionStorage; +import de.cuioss.portal.ui.api.exceptions.DefaultErrorMessage; +import de.cuioss.portal.ui.api.ui.pages.ErrorPage; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesError; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Portal base implementation of {@link ErrorPage} Note: It is assumed + * that the {@link DefaultErrorMessage} can be derived from the + * {@link PortalSessionStorage} with the key + * {@link DefaultErrorMessage#LOOKUP_KEY}. While retrieving the + * {@link DefaultErrorMessage} it will implicitly be removed. + * + * @author Oliver Wolff + */ +@PortalCorePagesError +@RequestScoped +@Priority(PortalPriorities.PORTAL_CORE_LEVEL) +@Named(ErrorPage.BEAN_NAME) +@EqualsAndHashCode(of = "message", doNotUseGetters = true) +@ToString(of = "message", doNotUseGetters = true) +public class ErrorPageBean implements ErrorPage { + + private static final long serialVersionUID = -3785494532638995890L; + + @Inject + @PortalSessionStorage + private MapStorage mapStorage; + + @Inject + private FacesContext context; + + @Getter + private DefaultErrorMessage message; + + /** + * Retrieve and removes the {@link DefaultErrorMessage} found under the key + * {@link DefaultErrorMessage#LOOKUP_KEY} + */ + @PostConstruct + public void init() { + if (mapStorage.containsKey(DefaultErrorMessage.LOOKUP_KEY)) { + message = (DefaultErrorMessage) mapStorage.remove(DefaultErrorMessage.LOOKUP_KEY); + } + ServletAdapterUtil.getResponse(context).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Override + public boolean isMessageAvailable() { + return null != message; + } + +} diff --git a/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http401PageBean.java b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http401PageBean.java new file mode 100644 index 0000000..e958f6d --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http401PageBean.java @@ -0,0 +1,62 @@ +package de.icw.cui.portal.ui.errorpages; + +import java.io.Serializable; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletResponse; + +import de.cuioss.portal.core.storage.MapStorage; +import de.cuioss.portal.core.storage.PortalSessionStorage; +import de.cuioss.portal.ui.api.exceptions.DefaultErrorMessage; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Page Bean for the error-code 401 (the request requires HTTP authentication) + * + * @author Oliver Wolff + * + */ +@RequestScoped +@Named +@EqualsAndHashCode(callSuper = true) +public class Http401PageBean extends AbstractHttpErrorPage { + + private static final long serialVersionUID = -2216275532091092216L; + + @Inject + @PortalSessionStorage + private MapStorage mapStorage; + + @Getter + private DefaultErrorMessage message; + + /** + * Initializes the view by determining the requestedUri and logging the + * errorCode at warn-level + * + * @return always {@code null} + */ + @Override + public String initView() { + super.initView(); + if (mapStorage.containsKey(DefaultErrorMessage.LOOKUP_KEY)) { + message = (DefaultErrorMessage) mapStorage.remove(DefaultErrorMessage.LOOKUP_KEY); + } + return null; + } + + @Override + protected int getErrorCode() { + return HttpServletResponse.SC_UNAUTHORIZED; + } + + /** + * @return flag indicating whether a message is available / to be displayed + */ + public boolean isMessageAvailable() { + return null != message; + } +} diff --git a/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http403PageBean.java b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http403PageBean.java new file mode 100644 index 0000000..55efbc0 --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http403PageBean.java @@ -0,0 +1,24 @@ +package de.icw.cui.portal.ui.errorpages; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Named; +import javax.servlet.http.HttpServletResponse; + +/** + * Page Bean for the error-code 403 (Forbidden) + * + * @author Oliver Wolff + * + */ +@RequestScoped +@Named +public class Http403PageBean extends AbstractHttpErrorPage { + + private static final long serialVersionUID = -2216275532091092216L; + + @Override + protected int getErrorCode() { + return HttpServletResponse.SC_FORBIDDEN; + } + +} diff --git a/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http404PageBean.java b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http404PageBean.java new file mode 100644 index 0000000..8092b97 --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/java/de/icw/cui/portal/ui/errorpages/Http404PageBean.java @@ -0,0 +1,47 @@ +package de.icw.cui.portal.ui.errorpages; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import de.cuioss.portal.configuration.PortalConfigurationKeys; +import de.cuioss.tools.base.BooleanOperations; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Page Bean for the error-code 404 (Resource not found) + * + * @author Oliver Wolff + * + */ +@RequestScoped +@Named +@EqualsAndHashCode(callSuper = true) +public class Http404PageBean extends AbstractHttpErrorPage { + + private static final long serialVersionUID = -2216275532091092216L; + + @Inject + @ConfigProperty(name = PortalConfigurationKeys.PAGES_ERROR_404_REDIRECT) + @Getter + private boolean shouldRedirect; + + @Override + public String initView() { + super.initView(); + if (BooleanOperations.isAnyTrue(!isJsfView(), !shouldRedirect, !isRequestUriAvailable())) { + shouldRedirect = false; + } + return null; + } + + @Override + protected int getErrorCode() { + return HttpServletResponse.SC_NOT_FOUND; + } + +} diff --git a/modules/portal-ui-errorpages/src/main/resources/META-INF/beans.xml b/modules/portal-ui-errorpages/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..c76b093 --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/401.xhtml b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/401.xhtml new file mode 100644 index 0000000..c547e7b --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/401.xhtml @@ -0,0 +1,36 @@ + + + + + + + + + + + + #{msgs['page.401.title']} + + +

      #{msgs['page.401.srHeader']}

      +

      #{msgs['page.401.title']}

      + +

      #{http401PageBean.requestUri}

      +
      + +

      + +

      +

      + +

      +
      + +
      +
      + diff --git a/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/403.xhtml b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/403.xhtml new file mode 100644 index 0000000..32d5dc0 --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/403.xhtml @@ -0,0 +1,27 @@ + + + + + + + + + + + + #{msgs['page.403.title']} + + +

      #{msgs['page.403.srHeader']}

      +

      #{msgs['page.403.title']}

      + +

      #{http403PageBean.requestUri}

      +
      + +
      +
      + diff --git a/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/404.xhtml b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/404.xhtml new file mode 100644 index 0000000..90590dd --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/404.xhtml @@ -0,0 +1,36 @@ + + + + + + + + + + + #{msgs['page.404.title']} + + +

      #{msgs['page.404.srHeader']}

      +

      #{msgs['page.404.title']}

      + + +

      #{http404PageBean.requestUri}

      +
      +
      + + +

      #{msgs['page.404.youWillBeRedirected']}

      + +
      +
      +
      + diff --git a/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/error.xhtml b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/error.xhtml new file mode 100644 index 0000000..43f54ef --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/resources/META-INF/faces/guest/error.xhtml @@ -0,0 +1,34 @@ + + + + + + #{msgs['page.error.title']} + + +

      #{msgs['page.error.srHeader']}

      +

      #{msgs['page.error.title']}

      + +

      + +

      +

      + +

      +
      + + +

      + +

      +
      +

      + +

      +
      +
      + diff --git a/modules/portal-ui-errorpages/src/main/resources/META-INF/web-fragment.xml b/modules/portal-ui-errorpages/src/main/resources/META-INF/web-fragment.xml new file mode 100644 index 0000000..f4226f9 --- /dev/null +++ b/modules/portal-ui-errorpages/src/main/resources/META-INF/web-fragment.xml @@ -0,0 +1,32 @@ + + + + CuiPortalErrorPages + + + + 401 + /faces/guest/401.jsf + + + + 403 + /faces/guest/403.jsf + + + + 404 + /faces/guest/404.jsf + + + + 500 + /faces/guest/error.jsf + + \ No newline at end of file diff --git a/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/ErrorPageBeanTest.java b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/ErrorPageBeanTest.java new file mode 100644 index 0000000..85f096b --- /dev/null +++ b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/ErrorPageBeanTest.java @@ -0,0 +1,48 @@ +package de.icw.cui.portal.ui.errorpages; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.core.storage.PortalSessionStorage; +import de.cuioss.portal.core.test.mocks.core.PortalSessionStorageMock; +import de.cuioss.portal.ui.api.exceptions.DefaultErrorMessage; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesError; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import lombok.Getter; + +@EnablePortalUiEnvironment +class ErrorPageBeanTest extends AbstractPageBeanTest { + + @Inject + @PortalSessionStorage + private PortalSessionStorageMock mapStorage; + + @Inject + @PortalCorePagesError + @Getter + private ErrorPageBean underTest; + + @Test + void shouldIgnoreNotExisitingMessage() { + assertFalse(underTest.isMessageAvailable()); + } + + @Test + void shouldResolveAndRemoveErrorMessage() { + var errorMessage = new DefaultErrorMessage("errorCode", "errorTicket", "errorMessage", + "pageId"); + DefaultErrorMessage.addErrorMessageToSessionStorage(errorMessage, mapStorage); + + assertTrue(mapStorage.containsKey(DefaultErrorMessage.LOOKUP_KEY)); + assertTrue(underTest.isMessageAvailable()); + assertEquals(errorMessage, underTest.getMessage()); + assertFalse(mapStorage.containsKey(DefaultErrorMessage.LOOKUP_KEY)); + } + +} diff --git a/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http401PageBeanTest.java b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http401PageBeanTest.java new file mode 100644 index 0000000..2fb9ddb --- /dev/null +++ b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http401PageBeanTest.java @@ -0,0 +1,27 @@ +package de.icw.cui.portal.ui.errorpages; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.core.test.mocks.core.PortalSessionStorageMock; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import lombok.Getter; + +@EnablePortalUiEnvironment +@AddBeanClasses({ PortalSessionStorageMock.class }) +class Http401PageBeanTest extends AbstractPageBeanTest { + + @Getter + @Inject + private Http401PageBean underTest; + + @Test + void shouldProvideCorrectCode() { + assertEquals(401, underTest.getErrorCode()); + } +} diff --git a/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http403PageBeanTest.java b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http403PageBeanTest.java new file mode 100644 index 0000000..17c5be2 --- /dev/null +++ b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http403PageBeanTest.java @@ -0,0 +1,24 @@ +package de.icw.cui.portal.ui.errorpages; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import lombok.Getter; + +@EnablePortalUiEnvironment +class Http403PageBeanTest extends AbstractPageBeanTest { + + @Getter + @Inject + private Http403PageBean underTest; + + @Test + void shouldProvideCorrectCode() { + assertEquals(403, underTest.getErrorCode()); + } +} diff --git a/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http404PageBeanTest.java b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http404PageBeanTest.java new file mode 100644 index 0000000..a3a2737 --- /dev/null +++ b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/Http404PageBeanTest.java @@ -0,0 +1,94 @@ +package de.icw.cui.portal.ui.errorpages; + +import static de.icw.cui.portal.ui.errorpages.AbstractHttpErrorPage.JAVAX_SERVLET_ERROR_REQUEST_URI; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.configuration.PortalConfigurationKeys; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import lombok.Getter; + +@EnablePortalUiEnvironment +class Http404PageBeanTest extends AbstractPageBeanTest { + + private static final String FACES_VIEW_JSF = "/faces/view.jsf"; + private static final String NON_FACES_VIEW_JSF = "/some/view.html"; + + @Inject + @Getter + private Http404PageBean underTest; + + @Test + void shouldProvideCorrectCode() { + assertEquals(404, underTest.getErrorCode()); + } + + @Test + void shouldDetectFacesView() { + getRequestConfigDecorator().setViewId(FACES_VIEW_JSF).setRequestAttribute(JAVAX_SERVLET_ERROR_REQUEST_URI, + FACES_VIEW_JSF); + underTest.initView(); + assertTrue(underTest.isJsfView()); + assertTrue(underTest.isRequestUriAvailable()); + assertTrue(underTest.isShouldRedirect()); + } + + @Test + void shouldDetectNonFacesView() { + getRequestConfigDecorator().setViewId(NON_FACES_VIEW_JSF).setRequestAttribute(JAVAX_SERVLET_ERROR_REQUEST_URI, + NON_FACES_VIEW_JSF); + underTest.initView(); + assertFalse(underTest.isJsfView()); + assertTrue(underTest.isRequestUriAvailable()); + assertFalse(underTest.isShouldRedirect()); + } + + @Test + void shouldDetectEmptyView() { + getRequestConfigDecorator().setViewId("").setRequestAttribute(JAVAX_SERVLET_ERROR_REQUEST_URI, ""); + underTest.initView(); + assertFalse(underTest.isJsfView()); + assertFalse(underTest.isRequestUriAvailable()); + assertFalse(underTest.isShouldRedirect()); + } + + @Test + void shouldHandleNotSetView() { + getRequestConfigDecorator().setViewId(null); + underTest.initView(); + assertFalse(underTest.isJsfView()); + assertFalse(underTest.isRequestUriAvailable()); + assertFalse(underTest.isShouldRedirect()); + } + + /** + * Paranoia mode. Should not happen, but error-handling must always be defensive + */ + @Test + void shouldHandleMissingServletRequest() { + getRequestConfigDecorator().setViewId(null); + getExternalContext().setRequest(null); + underTest.initView(); + assertFalse(underTest.isJsfView()); + assertFalse(underTest.isRequestUriAvailable()); + assertFalse(underTest.isShouldRedirect()); + } + + @Test + void shouldNotRedirectIfNotConfigured() { + configuration.fireEvent(PortalConfigurationKeys.PAGES_ERROR_404_REDIRECT, "false"); + + getRequestConfigDecorator().setViewId(FACES_VIEW_JSF).setRequestAttribute(JAVAX_SERVLET_ERROR_REQUEST_URI, + FACES_VIEW_JSF); + underTest.initView(); + assertTrue(underTest.isJsfView()); + assertTrue(underTest.isRequestUriAvailable()); + assertFalse(underTest.isShouldRedirect()); + } +} diff --git a/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/ModuleConsistencyTest.java b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/ModuleConsistencyTest.java new file mode 100644 index 0000000..c7e410f --- /dev/null +++ b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/ModuleConsistencyTest.java @@ -0,0 +1,15 @@ +package de.icw.cui.portal.ui.errorpages; + +import org.jboss.weld.environment.se.Weld; + +import de.cuioss.portal.core.test.tests.BaseModuleConsistencyTest; +import de.cuioss.test.jsf.producer.JsfObjectsProducers; +import de.cuioss.test.jsf.producer.ServletObjectsFromJSFContextProducers; + +class ModuleConsistencyTest extends BaseModuleConsistencyTest { + + @Override + protected Weld modifyWeldContainer(Weld weld) { + return weld.addBeanClasses(ServletObjectsFromJSFContextProducers.class, JsfObjectsProducers.class); + } +} diff --git a/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/PortalViewMapperSimpleTest.java b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/PortalViewMapperSimpleTest.java new file mode 100644 index 0000000..c20defc --- /dev/null +++ b/modules/portal-ui-errorpages/src/test/java/de/icw/cui/portal/ui/errorpages/PortalViewMapperSimpleTest.java @@ -0,0 +1,47 @@ +package de.icw.cui.portal.ui.errorpages; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; + +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.ui.api.templating.PortalMultiViewMapper; +import de.cuioss.portal.ui.runtime.application.templating.PortalViewMapper; +import de.cuioss.test.valueobjects.junit5.contracts.ShouldHandleObjectContracts; +import lombok.Getter; + +@EnableAutoWeld +class PortalViewMapperSimpleTest implements ShouldHandleObjectContracts { + + public static final String PORTAL = "/META-INF/faces/"; + + public static final String UNAUTHORIZED = "guest/401.xhtml"; + public static final String FORBIDDEN = "guest/403.xhtml"; + public static final String NOT_FOUND = "guest/404.xhtml"; + public static final String ERROR = "guest/error.xhtml"; + + public static final String NOT_THERE = "not.there.xhtml"; + + @Inject + @PortalMultiViewMapper + @Getter + private PortalViewMapper underTest; + + @Test + void shouldInitCorrectly() { + assertTrue(underTest.resolveViewPath(ERROR).getPath().endsWith(PORTAL + ERROR)); + assertTrue(underTest.resolveViewPath(UNAUTHORIZED).getPath().endsWith(PORTAL + UNAUTHORIZED)); + assertTrue(underTest.resolveViewPath(FORBIDDEN).getPath().endsWith(PORTAL + FORBIDDEN)); + assertTrue(underTest.resolveViewPath(NOT_FOUND).getPath().endsWith(PORTAL + NOT_FOUND)); + } + + @Test + void shouldFailOnNoneExistingResource() { + assertThrows(NullPointerException.class, () -> { + assertTrue(underTest.resolveViewPath(NOT_THERE).getPath().endsWith(PORTAL + NOT_THERE)); + }); + } +} diff --git a/modules/portal-ui-form-based-login/pom.xml b/modules/portal-ui-form-based-login/pom.xml new file mode 100644 index 0000000..f513023 --- /dev/null +++ b/modules/portal-ui-form-based-login/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + de.cuioss.portal.ui + modules + 1.0.0-SNAPSHOT + + portal-ui-form-based-login + Portal UI Form-based Login + Provides a form based login and logout page + + + de.cuioss.portal.ui + portal-ui-api + + + de.cuioss.portal.ui + portal-ui-runtime + compile + + + + de.cuioss.portal.ui + portal-ui-unit-testing + + + de.cuioss.portal.test + portal-core-unit-testing + + + \ No newline at end of file diff --git a/modules/portal-ui-form-based-login/src/main/java/de/cuioss/portal/ui/authentication/form/LoginPageBean.java b/modules/portal-ui-form-based-login/src/main/java/de/cuioss/portal/ui/authentication/form/LoginPageBean.java new file mode 100644 index 0000000..b032e01 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/java/de/cuioss/portal/ui/authentication/form/LoginPageBean.java @@ -0,0 +1,216 @@ +package de.cuioss.portal.ui.authentication.form; + +import static de.cuioss.portal.configuration.PortalConfigurationKeys.PAGES_LOGIN_DEFAULT_USERSTORE; +import static de.cuioss.portal.ui.api.GlobalComponentIds.LOGIN_PAGE_USER_NAME; +import static de.cuioss.portal.ui.api.GlobalComponentIds.LOGIN_PAGE_USER_PASSWORD; +import static de.cuioss.tools.string.MoreStrings.isEmpty; + +import java.util.List; +import java.util.Optional; + +import javax.annotation.PostConstruct; +import javax.annotation.Priority; +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.omnifaces.cdi.Param; + +import de.cuioss.jsf.api.application.message.DisplayNameProviderMessageProducer; +import de.cuioss.jsf.api.application.message.MessageProducer; +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.PortalUser; +import de.cuioss.portal.authentication.facade.FormBasedAuthenticationFacade; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.model.UserStore; +import de.cuioss.portal.configuration.common.PortalPriorities; +import de.cuioss.portal.ui.api.message.PortalMessageProducer; +import de.cuioss.portal.ui.api.ui.pages.HomePage; +import de.cuioss.portal.ui.api.ui.pages.LoginPage; +import de.cuioss.portal.ui.api.ui.pages.LoginPageClientStorage; +import de.cuioss.portal.ui.api.ui.pages.LoginPageHistoryManagerProvider; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogin; +import de.cuioss.portal.ui.runtime.page.AbstractLoginPageBean; +import de.cuioss.portal.ui.runtime.page.PortalPagesConfiguration; +import de.cuioss.uimodel.application.LoginCredentials; +import de.cuioss.uimodel.nameprovider.IDisplayNameProvider; +import de.cuioss.uimodel.result.ResultObject; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Page bean for the login. it is {@link RequestScoped} in order to be used with + * non-transient views. + */ +@PortalCorePagesLogin +@Named(LoginPage.BEAN_NAME) +@Priority(PortalPriorities.PORTAL_CORE_LEVEL) +@RequestScoped +@EqualsAndHashCode(of = { "loginCredentials", "availableUserStores" }, doNotUseGetters = true, callSuper = false) +@ToString(of = { "loginCredentials", "availableUserStores" }, doNotUseGetters = true) +public class LoginPageBean extends AbstractLoginPageBean implements LoginPage { + + private static final long serialVersionUID = 8709729494565906154L; + + @Getter + private LoginCredentials loginCredentials; + + @SuppressWarnings("cdi-ambiguous-dependency") + @Inject + @Param + private String username; + + @SuppressWarnings("cdi-ambiguous-dependency") + @Inject + @Param + private String userstore; + + @Getter + private List availableUserStores; + + @SuppressWarnings("cdi-ambiguous-dependency") + @Inject + @PortalAuthenticationFacade + private FormBasedAuthenticationFacade authenticationFacade; + + @Inject + private LoginPageHistoryManagerProvider historyManagerProvider; + + @Inject + private LoginPageClientStorage localStorage; + + @Inject + @PortalMessageProducer + private MessageProducer messageProducer; + + @Inject + @PortalUser + private AuthenticatedUserInfo userInfo; + + @Inject + private Provider facesContextProvider; + + @Inject + private PortalPagesConfiguration pagesConfiguration; + + @Inject + @ConfigProperty(name = PAGES_LOGIN_DEFAULT_USERSTORE) + private Optional defaultConfiguredUserStore; + + @Getter + @Setter + private String errorTextKey; + + /** + * Initializes the availableUserStores and {@link LoginCredentials} + */ + @PostConstruct + public void init() { + + availableUserStores = authenticationFacade.getAvailableUserStores(); + + loginCredentials = historyManagerProvider.extractFromDeepLinkingUrlParameter(userstore, username) + .orElseGet(localStorage.extractFromClientStorage()); + + checkUserStoreAndAdjustIfNeeded(); + } + + private void checkUserStoreAndAdjustIfNeeded() { + if (!availableUserStores.isEmpty() && isEnteredUserStoreEmptyOrInvalid()) { + + final var userStoreFromCookie = localStorage.extractFromClientStorage().get().getUserStore(); + + if (isUserStoreValueValid(userStoreFromCookie)) { + loginCredentials.setUserStore(userStoreFromCookie); + } else { + loginCredentials.setUserStore(defaultUserStore()); + } + } + } + + private boolean isEnteredUserStoreEmptyOrInvalid() { + return !isUserStoreValueValid(loginCredentials.getUserStore()); + } + + @SuppressWarnings("squid:S3655") // owolff: Optional is checked properly + private String defaultUserStore() { + if (defaultConfiguredUserStore.isPresent() && isUserStoreValueValid(defaultConfiguredUserStore.get())) { + return defaultConfiguredUserStore.get(); + } + return availableUserStores.get(0).getName(); + } + + private boolean isUserStoreValueValid(final String userStoreFromCookie) { + return availableUserStores.stream().anyMatch(userStore -> userStore.getName().equals(userStoreFromCookie)); + } + + @Override + public String initViewAction() { + String outcome = null; + if (userInfo.isAuthenticated()) { + final var strategy = pagesConfiguration.getLoginPageStrategy(); + switch (strategy) { + case GOTO_HOME: + outcome = HomePage.OUTCOME; + break; + case LOGOUT: + authenticationFacade.logout(ServletAdapterUtil.getRequest(facesContextProvider.get())); + break; + default: + throw new IllegalStateException("Unknown LoginPageStrategy found: " + strategy); + } + } + if (!isEmpty(errorTextKey)) { + messageProducer.setGlobalErrorMessage(errorTextKey); + } + return outcome; + } + + @Override + public String getFocusComponent() { + if (isEmpty(loginCredentials.getUsername())) { + return LOGIN_PAGE_USER_NAME.getId(); + } + return LOGIN_PAGE_USER_PASSWORD.getId(); + } + + @Override + public String login() { + + localStorage.updateLocalStored(loginCredentials); + + return loginAction(() -> historyManagerProvider.getCurrentViewExcludeUserStoreAndUserName(), + ServletAdapterUtil.getRequest(facesContextProvider.get()), facesContextProvider.get()); + } + + @Override + public boolean isShouldDisplayUserStoreDropdown() { + return availableUserStores.size() > 1; + } + + @Override + protected ResultObject doLogin(final HttpServletRequest currentServletRequest) { + return authenticationFacade.login(currentServletRequest, loginCredentials); + } + + @Override + protected void handleLoginFailed(final IDisplayNameProvider reason) { + // FIXME: MWL reactivate + // if (!clientInformation.canIUse(FeatureName.COOKIES)) { + // messageProducer.setGlobalErrorMessage("message.error.cookies.disable"); + // } + if (null != reason) { + new DisplayNameProviderMessageProducer(messageProducer).showAsGlobalMessage(reason, + FacesMessage.SEVERITY_ERROR); + } + } + +} diff --git a/modules/portal-ui-form-based-login/src/main/java/de/cuioss/portal/ui/authentication/form/LogoutPageBean.java b/modules/portal-ui-form-based-login/src/main/java/de/cuioss/portal/ui/authentication/form/LogoutPageBean.java new file mode 100644 index 0000000..28d72dd --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/java/de/cuioss/portal/ui/authentication/form/LogoutPageBean.java @@ -0,0 +1,68 @@ +package de.cuioss.portal.ui.authentication.form; + +import javax.annotation.Priority; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.event.Event; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; + +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.LoginEvent; +import de.cuioss.portal.authentication.PortalLoginEvent; +import de.cuioss.portal.authentication.PortalUser; +import de.cuioss.portal.authentication.facade.AuthenticationFacade; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.configuration.common.PortalPriorities; +import de.cuioss.portal.ui.api.ui.pages.LoginPage; +import de.cuioss.portal.ui.api.ui.pages.LogoutPage; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogout; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * @author Oliver Wolff + */ +@PortalCorePagesLogout +@Named(LogoutPage.BEAN_NAME) +@Priority(PortalPriorities.PORTAL_CORE_LEVEL) +@RequestScoped +@EqualsAndHashCode(doNotUseGetters = true) +@ToString(doNotUseGetters = true) +public class LogoutPageBean implements LogoutPage { + + private static final long serialVersionUID = -3588577094632702649L; + + @Inject + @PortalAuthenticationFacade + private AuthenticationFacade authenticationFacade; + + @Inject + private FacesContext facesContext; + + @Inject + @PortalLoginEvent + private Event preLogoutEvent; + + @Inject + @PortalUser + private AuthenticatedUserInfo authenticatedUserInfo; + + /** + * Logs out and redirects to login page. + */ + @Override + public String logoutViewAction() { + performLogout(); + return LoginPage.OUTCOME; + } + + @Override + public String performLogout() { + if (authenticatedUserInfo.isAuthenticated()) { + preLogoutEvent.fire(LoginEvent.builder().action(LoginEvent.Action.LOGOUT).build()); + } + return authenticationFacade.logout(ServletAdapterUtil.getRequest(facesContext)) ? LoginPage.OUTCOME : null; + } +} diff --git a/modules/portal-ui-form-based-login/src/main/resources/META-INF/beans.xml b/modules/portal-ui-form-based-login/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..d54a394 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces-config.xml b/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces-config.xml new file mode 100644 index 0000000..6df8f7f --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces-config.xml @@ -0,0 +1,24 @@ + + + CuiPortalFormBasedLogin + + * + + login + /faces/guest/login.xhtml + + + + + + * + + logout + /faces/guest/logout.xhtml + + + + diff --git a/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces/guest/login.xhtml b/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces/guest/login.xhtml new file mode 100644 index 0000000..5d5bd3f --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces/guest/login.xhtml @@ -0,0 +1,145 @@ + + + + + + + + + + + #{msgs['page.login.title']} + + + + + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces/guest/logout.xhtml b/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces/guest/logout.xhtml new file mode 100644 index 0000000..fbdb743 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/resources/META-INF/faces/guest/logout.xhtml @@ -0,0 +1,37 @@ + + + + + + + + + + + + #{msgs['page.logout.title']} + + + +
      +
      +

      #{msgs['page.logout.srHeader']}

      +
      +

      + +

      +

      + +

      +
      +
      +
      +
      +
      + diff --git a/modules/portal-ui-form-based-login/src/main/resources/META-INF/resources/de.cuioss.portal.ui.oauth/login_page.css b/modules/portal-ui-form-based-login/src/main/resources/META-INF/resources/de.cuioss.portal.ui.oauth/login_page.css new file mode 100644 index 0000000..1700255 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/main/resources/META-INF/resources/de.cuioss.portal.ui.oauth/login_page.css @@ -0,0 +1,141 @@ +.guest_login { + min-height: 450px; +} + +.login-wrapper .container { + max-width: 340px; +} + +.product-name, .module-name { + color: white; + font-weight: bold; + text-align: center; +} + +.form-login { + max-width: 320px; + color: white; + background-color: rgba(80, 80, 80, 0.8); + padding: 15px 15px 30px; +} + +.guest_login .navbar { + display: none; +} + +/* adapt layout of sticky-messages on login page */ +.login-wrapper .sticky-messages .single-sticky-message { + margin-bottom: 0; + background-color: rgba(255, 255, 255, 0.7); +} + +.login-wrapper .sticky-messages { + position: absolute; + top: 0; + width: 100%; + z-index: 1000; +} + +.login-wrapper .sticky-messages + .container .logo { + display: none; +} + +@media screen and (min-height: 600px) and (max-height: 720px) { + .form-login { + position: fixed; + width: 100%; + bottom: 5%; + } +} + +@media screen and (max-height: 599px) { + .logo { + display: none; + } +} + +/* xs media definition */ +@media screen and (min-width: 350px) and (max-width: 767px) { + .login-wrapper .container { + width: 95%; + } +} + +/* xxs media definition */ +@media screen and (min-width: 200px) and (max-width: 349px) { + .login-wrapper .container { + width: 95%; + margin-right: 1%; + } + + .form-login { + width: 100%; + } + + .login-wrapper + .page-footer { + background-color: rgba(80, 80, 80, 0.8); + color: white; + min-height: 25px; + } + + .login-wrapper + .page-footer p { + margin-top: 5px; + margin-bottom: 5px; + } +} + +/* mobile device adaptation */ +@media screen and (orientation: landscape) and (max-width: 767px) { + .login-wrapper .container { + padding-top: 10px; + } + + .login-wrapper .logo { + display: none; + } + + .form-login { + padding-bottom: 15px; + } + + .form-login .product-name { + margin-top: 0; + font-size: 16px; + } + + .form-login .module-name { + margin-bottom: 10px; + margin-top: 10px; + font-size: 14px; + } + + .form-login .form-group { + margin-bottom: 10px; + } + + .form-login hr ~ a:before { + clear: both; + } + + .form-login hr ~ a:last-of-type { + margin-top: 0; + } + + .form-login hr ~ a.btn-block { + width: 49%; + display: inline-block; + position: static; + } + + .login-wrapper + .page-footer { + background-color: rgba(80, 80, 80, 0.8); + color: white; + min-height: 25px; + } + + .login-wrapper + .page-footer p { + margin-top: 5px; + margin-bottom: 5px; + } +} + diff --git a/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/LoginPageBeanTest.java b/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/LoginPageBeanTest.java new file mode 100644 index 0000000..e0b5b35 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/LoginPageBeanTest.java @@ -0,0 +1,256 @@ +package de.cuioss.portal.ui.authentication.form; + +import static de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock.DEFAULT_USER_STORE; +import static de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock.SOME_LDAP_USER_STORE; +import static de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock.SOME_OTHER_LDAP_USER_STORE; +import static de.cuioss.tools.collect.CollectionLiterals.mutableList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.omnifaces.cdi.Param; + +import de.cuioss.jsf.api.application.bundle.CuiResourceBundle; +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.jsf.api.common.view.ViewDescriptorImpl; +import de.cuioss.jsf.api.converter.nameprovider.LabeledKeyConverter; +import de.cuioss.jsf.test.mock.application.MirrorCuiRessourcBundle; +import de.cuioss.portal.authentication.LoginEvent; +import de.cuioss.portal.authentication.PortalLoginEvent; +import de.cuioss.portal.authentication.facade.AuthenticationResults; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.configuration.PortalConfigurationKeys; +import de.cuioss.portal.configuration.PortalConfigurationSource; +import de.cuioss.portal.core.storage.PortalSessionStorage; +import de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock; +import de.cuioss.portal.core.test.mocks.authentication.PortalTestUserProducer; +import de.cuioss.portal.core.test.mocks.configuration.PortalTestConfiguration; +import de.cuioss.portal.core.test.mocks.core.PortalClientStorageMock; +import de.cuioss.portal.core.test.mocks.core.PortalSessionStorageMock; +import de.cuioss.portal.ui.api.history.PortalHistoryManager; +import de.cuioss.portal.ui.api.message.PortalMessageProducer; +import de.cuioss.portal.ui.api.ui.pages.HomePage; +import de.cuioss.portal.ui.api.ui.pages.LoginPage; +import de.cuioss.portal.ui.api.ui.pages.LoginPageStrategy; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogin; +import de.cuioss.portal.ui.runtime.page.LoginPageClientStorageImpl; +import de.cuioss.portal.ui.runtime.page.LoginPageHistoryManagerProviderImpl; +import de.cuioss.portal.ui.runtime.page.PortalPagesConfiguration; +import de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.mocks.PortalHistoryManagerMock; +import de.cuioss.portal.ui.test.mocks.PortalMessageProducerMock; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import de.cuioss.test.jsf.config.BeanConfigurator; +import de.cuioss.test.jsf.config.ComponentConfigurator; +import de.cuioss.test.jsf.config.decorator.BeanConfigDecorator; +import de.cuioss.test.jsf.config.decorator.ComponentConfigDecorator; +import de.cuioss.tools.net.UrlParameter; +import de.cuioss.uimodel.nameprovider.LabeledKey; +import lombok.Getter; + +@EnablePortalUiEnvironment +@AddBeanClasses({ PortalPagesConfiguration.class, PortalTestUserProducer.class, PortalClientStorageMock.class, + LoginPageClientStorageImpl.class, LoginPageHistoryManagerProviderImpl.class }) +class LoginPageBeanTest extends AbstractPageBeanTest implements ComponentConfigurator, BeanConfigurator { + + private static final String SOME_ERROR_KEY = "some.error"; + + private static final String TEST_USER_STORE = SOME_LDAP_USER_STORE.getName(); + + private static final String TEST_USER_NAME = "testUserName"; + + @Inject + @PortalCorePagesLogin + @Getter + private LoginPageBean underTest; + + @Inject + @PortalAuthenticationFacade + private PortalAuthenticationFacadeMock authenticationFacadeMock; + + @Inject + @PortalMessageProducer + private PortalMessageProducerMock messageProducerMock; + + @Inject + @PortalHistoryManager + private PortalHistoryManagerMock portalHistoryManagerMock; + + @Inject + @PortalSessionStorage + private PortalSessionStorageMock mapStorage; + + @Inject + @PortalConfigurationSource + private PortalTestConfiguration configuration; + + private String username; + private String userstore; + + private LoginEvent event; + + @Produces + @Param + public String getParameter(final InjectionPoint injectionPoint) { + + configuration.put(PortalConfigurationKeys.PAGES_LOGIN_DEFAULT_USERSTORE, SOME_OTHER_LDAP_USER_STORE.getName()); + configuration.fireEvent(); + + final var name = injectionPoint.getMember().getName(); + switch (name) { + case LoginPage.KEY_USERNAME: + return username; + case LoginPage.KEY_USERSTORE: + return userstore; + default: + return null; + } + + } + + @Test + void shouldUseUrlParameter() { + username = TEST_USER_NAME; + userstore = TEST_USER_STORE; + + assertEquals(username, underTest.getLoginCredentials().getUsername()); + assertEquals(userstore, underTest.getLoginCredentials().getUserStore()); + } + + @Test + void shouldUseUrlParameterAtDeepLinking() { + + authenticationFacadeMock.logout(null); + getRequestConfigDecorator().setViewId(PortalNavigationConfiguration.VIEW_LOGIN_LOGICAL_VIEW_ID); + + // Mimic that Preferences was initially called + final ViewDescriptor newDescriptor = ViewDescriptorImpl.builder() + .withViewId(PortalNavigationConfiguration.VIEW_PREFERENCES_LOGICAL_VIEW_ID) + .withUrlParameter(mutableList(new UrlParameter(LoginPage.KEY_USERNAME, TEST_USER_NAME), + new UrlParameter(LoginPage.KEY_USERSTORE, TEST_USER_STORE))) + .withLogicalViewId(PortalNavigationConfiguration.VIEW_PREFERENCES_LOGICAL_VIEW_ID).build(); + portalHistoryManagerMock.addCurrentUriToHistory(newDescriptor); + + assertEquals(TEST_USER_NAME, underTest.getLoginCredentials().getUsername()); + assertEquals(TEST_USER_STORE, underTest.getLoginCredentials().getUserStore()); + underTest.getLoginCredentials().setPassword(TEST_USER_NAME); + + underTest.login(); + assertRedirect(PortalNavigationConfiguration.VIEW_PREFERENCES_LOGICAL_VIEW_ID); + assertNotNull(event); + assertEquals(LoginEvent.Action.LOGIN_SUCCESS, event.getAction()); + } + + @Test + void shouldRedirectToRequestedUrlAfterSuccessfulLogin() { + + authenticationFacadeMock.logout(null); + getRequestConfigDecorator().setViewId(PortalNavigationConfiguration.VIEW_LOGIN_LOGICAL_VIEW_ID); + // Mimic that Preferences was initially called + portalHistoryManagerMock.addCurrentUriToHistory(PortalNavigationConfiguration.DESCRIPTOR_PREFERENCES); + + underTest.getLoginCredentials().setPassword(PortalAuthenticationFacadeMock.USER); + underTest.getLoginCredentials().setUsername(PortalAuthenticationFacadeMock.USER); + + underTest.login(); + assertRedirect(PortalNavigationConfiguration.VIEW_PREFERENCES_LOGICAL_VIEW_ID); + } + + @Test + void shouldReturnHomeAfterSuccessfulLogin() { + + authenticationFacadeMock.logout(null); + getRequestConfigDecorator().setViewId(PortalNavigationConfiguration.VIEW_LOGIN_LOGICAL_VIEW_ID); + underTest.getLoginCredentials().setPassword(PortalAuthenticationFacadeMock.USER); + underTest.getLoginCredentials().setUsername(PortalAuthenticationFacadeMock.USER); + + underTest.login(); + assertRedirect(PortalNavigationConfiguration.VIEW_HOME_LOGICAL_VIEW_ID); + } + + @Test + void shouldFailOnInvalidLoginCredentials() { + + authenticationFacadeMock.logout(null); + getRequestConfigDecorator().setViewId(PortalNavigationConfiguration.VIEW_LOGIN_LOGICAL_VIEW_ID); + underTest.getLoginCredentials().setPassword(PortalAuthenticationFacadeMock.ADMIN); + underTest.getLoginCredentials().setUsername(PortalAuthenticationFacadeMock.USER); + assertNull(underTest.login()); + messageProducerMock.assertSingleGlobalMessageWithKeyPresent(AuthenticationResults.KEY_INVALID_CREDENTIALS); + assertNotNull(event); + assertEquals(LoginEvent.Action.LOGIN_FAILED, event.getAction()); + } + + @Test + void shouldDetermineWhetherToDisplayUserStore() { + assertTrue(underTest.isShouldDisplayUserStoreDropdown()); + } + + @Test + void shouldHideUserStoreSelectionOnSingleEntry() { + + // simulate only one user store is available + authenticationFacadeMock.setAvailableUserStores(mutableList(DEFAULT_USER_STORE)); + + underTest.init(); + + assertFalse(underTest.isShouldDisplayUserStoreDropdown(), "User store drop-down should be hidden"); + + assertEquals(DEFAULT_USER_STORE.getName(), underTest.getLoginCredentials().getUserStore(), + "On single user store the first must be selected as default"); + } + + @Test + void viewActionShouldReturnHomeOnDefaultStrategy() { + assertEquals(HomePage.OUTCOME, underTest.initViewAction()); + authenticationFacadeMock.assertAuthenticated(true); + } + + @Test + void shouldDisplayErrorText() { + underTest.setErrorTextKey(SOME_ERROR_KEY); + underTest.initViewAction(); + messageProducerMock.assertSingleGlobalMessageWithKeyPresent(SOME_ERROR_KEY); + } + + void viewActionShouldLogoutOnLogoutStrategy() { + configuration.put(PortalConfigurationKeys.PAGES_LOGIN_ENTER_STRATEGY, + LoginPageStrategy.LOGOUT.getStrategyName()); + configuration.fireEvent(); + assertNull(underTest.initViewAction()); + authenticationFacadeMock.assertAuthenticated(false); + } + + @Test + @Disabled // No idea here + void shouldUseConfiguredUserStoreAsDefault() { + assertEquals(underTest.getLoginCredentials().getUserStore(), SOME_OTHER_LDAP_USER_STORE.getName(), + "Wrong selected user store"); + } + + @Override + public void configureComponents(final ComponentConfigDecorator decorator) { + decorator.registerConverter(LabeledKeyConverter.class, LabeledKey.class); + } + + @Override + public void configureBeans(final BeanConfigDecorator decorator) { + decorator.register(new MirrorCuiRessourcBundle(), CuiResourceBundle.BEAN_NAME); + + } + + void onLoginEventListener(@Observes @PortalLoginEvent final LoginEvent givenEvent) { + event = givenEvent; + } +} diff --git a/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/LogoutPageBeanTest.java b/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/LogoutPageBeanTest.java new file mode 100644 index 0000000..d20d331 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/LogoutPageBeanTest.java @@ -0,0 +1,53 @@ +package de.cuioss.portal.ui.authentication.form; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import javax.enterprise.event.Observes; +import javax.inject.Inject; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.authentication.LoginEvent; +import de.cuioss.portal.authentication.PortalLoginEvent; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock; +import de.cuioss.portal.core.test.mocks.authentication.PortalTestUserProducer; +import de.cuioss.portal.ui.api.ui.pages.LoginPage; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogout; +import de.cuioss.portal.ui.runtime.page.PortalPagesConfiguration; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import lombok.Getter; + +@EnablePortalUiEnvironment +@AddBeanClasses({ PortalPagesConfiguration.class, PortalTestUserProducer.class }) +class LogoutPageBeanTest extends AbstractPageBeanTest { + + @Inject + @PortalCorePagesLogout + @Getter + private LogoutPageBean underTest; + + @Inject + @PortalAuthenticationFacade + private PortalAuthenticationFacadeMock authenticationFacadeMock; + + private LoginEvent event; + + @Test + void shouldLogoutOnViewAction() { + assertNull(event); + assertEquals(LoginPage.OUTCOME, underTest.logoutViewAction()); + authenticationFacadeMock.assertAuthenticated(false); + assertNotNull(event); + assertEquals(LoginEvent.Action.LOGOUT, event.getAction()); + } + + void onLoginEventListener(@Observes @PortalLoginEvent final LoginEvent givenEvent) { + event = givenEvent; + } + +} diff --git a/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/ModuleConsistencyTest.java b/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/ModuleConsistencyTest.java new file mode 100644 index 0000000..ecc908b --- /dev/null +++ b/modules/portal-ui-form-based-login/src/test/java/de/cuioss/portal/ui/authentication/form/ModuleConsistencyTest.java @@ -0,0 +1,17 @@ +package de.cuioss.portal.ui.authentication.form; + +import org.jboss.weld.environment.se.Weld; + +import de.cuioss.portal.core.test.mocks.authentication.PortalAuthenticationFacadeMock; +import de.cuioss.portal.core.test.tests.BaseModuleConsistencyTest; +import de.cuioss.test.jsf.producer.JsfObjectsProducers; +import de.cuioss.test.jsf.producer.ServletObjectsFromJSFContextProducers; + +class ModuleConsistencyTest extends BaseModuleConsistencyTest { + + @Override + protected Weld modifyWeldContainer(Weld weld) { + return weld.addBeanClasses(ServletObjectsFromJSFContextProducers.class, JsfObjectsProducers.class, + PortalAuthenticationFacadeMock.class); + } +} diff --git a/modules/portal-ui-form-based-login/src/test/resources/cui_logger.properties b/modules/portal-ui-form-based-login/src/test/resources/cui_logger.properties new file mode 100644 index 0000000..71c8451 --- /dev/null +++ b/modules/portal-ui-form-based-login/src/test/resources/cui_logger.properties @@ -0,0 +1,11 @@ +# ensures the catching of juli and jboss-logging will use slf4j as well as logger. +cui.logging.catch_all=true +cui.logging.formatter.print_stack_traces = false +# Suppress unnecessary info statements. +cui.logger.org.reflections.Reflections=warn +cui.logger.com.icw.ehf.cui.portal=warn +cui.logger.org.apache.deltaspike=warn +cui.logger.org.jboss.weld=warn +cui.logger.de.icw.cui.portal.configuration.impl=error +cui.logger.de.icw.cui.portal.configuration.installationpaths=error +cui.logger.com.icw.cui.test.valueobjects=warn \ No newline at end of file diff --git a/modules/portal-ui-oauth/pom.xml b/modules/portal-ui-oauth/pom.xml new file mode 100644 index 0000000..c44a449 --- /dev/null +++ b/modules/portal-ui-oauth/pom.xml @@ -0,0 +1,64 @@ + + 4.0.0 + + de.cuioss.portal.ui + modules + 1.0.0-SNAPSHOT + + portal-ui-oauth + Portal UI oauth + Provides ui infrastructure to use oauth2 + + 2.1.6 + + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs-api} + provided + + + + + + de.cuioss.portal.ui + portal-ui-api + + + de.cuioss.portal.ui + portal-ui-runtime + compile + + + de.cuioss.portal.authentication + portal-authentication-oauth + compile + + + org.eclipse.microprofile.rest.client + microprofile-rest-client-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + + de.cuioss.portal.ui + portal-ui-unit-testing + + + de.cuioss.portal.test + portal-core-unit-testing + + + org.jboss.resteasy + resteasy-cdi + test + + + \ No newline at end of file diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/LoginPagePathProducer.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/LoginPagePathProducer.java new file mode 100644 index 0000000..dce0cb8 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/LoginPagePathProducer.java @@ -0,0 +1,37 @@ +package de.cuioss.portal.ui.oauth; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.faces.context.FacesContext; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.ui.api.ui.pages.LoginPage; +import de.cuioss.tools.logging.CuiLogger; + +@RequestScoped +public class LoginPagePathProducer { + + private static final CuiLogger log = new CuiLogger(LoginPagePathProducer.class); + + /** Default login page based on cdi-portal-oauth faces-config.xml */ + private static final String DEFAULT = "/faces/guest/login.xhtml"; + + @Produces + @LoginPagePath + private String loginUrl = DEFAULT; + + /** + * Initialize the bean. + */ + @PostConstruct + public void init() { + final var context = FacesContext.getCurrentInstance(); + if (null != context) { + loginUrl = NavigationUtils.lookUpToLogicalViewIdBy(context, LoginPage.OUTCOME); + } else { + log.debug("No FacesContext available. Using URI: {}", loginUrl); + } + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/MissingScopesErrorDecoder.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/MissingScopesErrorDecoder.java new file mode 100644 index 0000000..4c7a6a7 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/MissingScopesErrorDecoder.java @@ -0,0 +1,90 @@ +package de.cuioss.portal.ui.oauth; + +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +import de.cuioss.tools.logging.CuiLogger; + +/** + * To detect and handle the error "missing scopes" + * (https://tools.ietf.org/html/rfc6750) + */ +public class MissingScopesErrorDecoder implements ResponseExceptionMapper { + + private static final CuiLogger log = new CuiLogger(MissingScopesErrorDecoder.class); + + private static final String WWW_AUTHENTICATE_HEADER_KEY = "www-authenticate"; + + /** + * @param status HTTP status code + * @param headers HTTP Headers + * + * @return MissingScopesException on HTTP 403 and existing www-authenticate + * header with missing scopes + */ + public static MissingScopesException checkAndHandleMissingScopes(final int status, + final MultivaluedMap headers) { + if (SC_FORBIDDEN == status) { + log.trace("response.status == 403"); + + var wwwAuthenticate = filterHeader(headers); + if (wwwAuthenticate.isEmpty()) { + log.debug("www-authenticate not found!"); + return null; + } + log.trace("www-authenticate found: {}", wwwAuthenticate); + + List wwwAuthenticateEntries = wwwAuthenticate.stream().map(value -> value.split(",")) + .flatMap(Arrays::stream).map(String::trim).collect(Collectors.toList()); + if (wwwAuthenticateEntries.stream().anyMatch(entry -> entry.equalsIgnoreCase("error=\"insufficient_scope\"") + || entry.equalsIgnoreCase("Bearer error=\"insufficient_scope\""))) { + var missingScopesEntry = wwwAuthenticateEntries.stream() + .filter(value -> value.trim().toLowerCase().startsWith("scope=\"")).findFirst(); + if (missingScopesEntry.isPresent() && missingScopesEntry.get().contains("=")) { + var missingScopesValue = missingScopesEntry.get().split("=")[1].replace('\"', ' ').trim(); + return new MissingScopesException(missingScopesValue); + } + log.debug("scopes entry is missing"); + } else { + log.debug("error=\"insufficient_scope\" is missing"); + } + } + return null; + } + + @Override + public MissingScopesException toThrowable(Response response) { + return checkAndHandleMissingScopes(response.getStatus(), response.getHeaders()); + } + + @Override + public boolean handles(int status, MultivaluedMap headers) { + if (SC_FORBIDDEN == status) { + log.trace("response.status == 403"); + + var wwwAuthenticate = filterHeader(headers); + if (wwwAuthenticate.isEmpty()) { + log.debug("www-authenticate not found!"); + return false; + } + return true; + } + return false; + } + + private static List filterHeader(MultivaluedMap headers) { + return headers.entrySet().stream().filter(entry -> entry.getKey().equalsIgnoreCase(WWW_AUTHENTICATE_HEADER_KEY)) + .map(Map.Entry::getValue).flatMap(Collection::stream).filter(value -> value instanceof String) + .map(value -> (String) value).collect(Collectors.toList()); + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/MissingScopesException.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/MissingScopesException.java new file mode 100644 index 0000000..65f034e --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/MissingScopesException.java @@ -0,0 +1,24 @@ +package de.cuioss.portal.ui.oauth; + +import de.cuioss.tools.logging.CuiLogger; +import lombok.Getter; +import lombok.Setter; + +public class MissingScopesException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 8581994138550480544L; + + private static final CuiLogger log = new CuiLogger(MissingScopesException.class); + + @Getter + @Setter + private String missingScopes; + + public MissingScopesException(String missingScopes) { + this.missingScopes = missingScopes; + log.debug("MissingScopesException: {}", missingScopes); + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthExceptionHandler.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthExceptionHandler.java new file mode 100644 index 0000000..67f1058 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthExceptionHandler.java @@ -0,0 +1,74 @@ +package de.cuioss.portal.ui.oauth; + +import java.io.Serializable; +import java.util.ResourceBundle; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.NavigationHandler; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.deltaspike.core.api.exception.control.ExceptionHandler; +import org.apache.deltaspike.core.api.exception.control.Handles; +import org.apache.deltaspike.core.api.exception.control.event.ExceptionEvent; + +import de.cuioss.jsf.api.application.message.MessageProducer; +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.portal.authentication.oauth.OauthAuthenticationException; +import de.cuioss.portal.core.bundle.PortalResourceBundle; +import de.cuioss.portal.core.storage.MapStorage; +import de.cuioss.portal.core.storage.PortalSessionStorage; +import de.cuioss.portal.ui.api.exceptions.DefaultErrorMessage; +import de.cuioss.portal.ui.api.message.PortalMessageProducer; +import de.cuioss.portal.ui.api.ui.context.CuiCurrentView; +import de.cuioss.portal.ui.api.ui.context.CuiNavigationHandler; + +/** + * @author Matthias Walliczek + */ +@ExceptionHandler +@Named +@RequestScoped +public class OauthExceptionHandler implements Serializable { + + private static final long serialVersionUID = 4117684215250330155L; + + private static final String OAUTH_ERROR_OUTCOME = "oauth-error"; + + @Inject + @PortalMessageProducer + private MessageProducer messageProducer; + + @Inject + @CuiCurrentView + private ViewDescriptor currentView; + + @Inject + private FacesContext facesContext; + + @Inject + @CuiNavigationHandler + private NavigationHandler navigationHandler; + + @Inject + @PortalSessionStorage + private MapStorage sessionStorage; + + @Inject + @PortalResourceBundle + private ResourceBundle resourceBundle; + + void handleOauthAuthenticationException( + @Handles(ordinal = 2) final ExceptionEvent event) { + DefaultErrorMessage.addErrorMessageToSessionStorage(createErrorMessage(event.getException().getMessage()), + this.sessionStorage); + this.navigationHandler.handleNavigation(this.facesContext, null, OAUTH_ERROR_OUTCOME); + event.handled(); + } + + protected DefaultErrorMessage createErrorMessage(final String messageKey) { + return new DefaultErrorMessage("", "", this.resourceBundle.getString(messageKey), this.currentView.getViewId()); + } + +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthHttpHeaderFilter.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthHttpHeaderFilter.java new file mode 100644 index 0000000..d936bae --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthHttpHeaderFilter.java @@ -0,0 +1,54 @@ +package de.cuioss.portal.ui.oauth; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.deltaspike.core.spi.activation.Deactivatable; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.portal.configuration.initializer.ApplicationInitializer; +import de.cuioss.portal.configuration.initializer.PortalInitializer; +import de.cuioss.portal.core.listener.literal.ServletInitialized; +import de.cuioss.tools.string.MoreStrings; + +/** + * Enable the token renewing by setting the cors header for the login page to be + * accessed via indirect Ajax call throu oauth server. + */ +@ApplicationScoped +@PortalInitializer +public class OauthHttpHeaderFilter implements ApplicationInitializer, Deactivatable { + + private static final String FACES_GUEST_LOGIN_JSF = "/faces/guest/login.jsf"; + + @Inject + private Provider requestProvider; + + @Override + public void initialize() { + // nothing to do + } + + /** + * @param response + */ + public void onCreate(@Observes @ServletInitialized final HttpServletResponse response) { + var request = requestProvider.get(); + var foundId = NavigationUtils.extractRequestUri(request); + if (FACES_GUEST_LOGIN_JSF.endsWith(foundId) + && !MoreStrings.isEmpty(requestProvider.get().getHeader("Origin"))) { + response.setHeader("Access-Control-Allow-Origin", requestProvider.get().getHeader("Origin")); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } + + } + + @Override + public Integer getOrder() { + return ORDER_LATE; + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthIFrameLogoutPageBean.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthIFrameLogoutPageBean.java new file mode 100644 index 0000000..b979707 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthIFrameLogoutPageBean.java @@ -0,0 +1,55 @@ +package de.cuioss.portal.ui.oauth; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.event.Event; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; + +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.LoginEvent; +import de.cuioss.portal.authentication.PortalLoginEvent; +import de.cuioss.portal.authentication.PortalUser; +import de.cuioss.portal.authentication.facade.AuthenticationFacade; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Provides an empty logout page to be embedded from the oauth server via iframe + * for the front channel logout - no further action is necessary. + * + * @author Matthias Walliczek + */ +@RequestScoped +@Named +@EqualsAndHashCode(of = "servletRequest", doNotUseGetters = true) +@ToString(of = "servletRequest", doNotUseGetters = true) +public class OauthIFrameLogoutPageBean { + + @Inject + @PortalAuthenticationFacade + private AuthenticationFacade authenticationFacade; + + @Inject + private HttpServletRequest servletRequest; + + @Inject + @PortalUser + private AuthenticatedUserInfo authenticatedUserInfo; + + @Inject + @PortalLoginEvent + private Event preLougoutEvent; + + /** + * @return + */ + public String logoutViewAction() { + if (authenticatedUserInfo.isAuthenticated()) { + preLougoutEvent.fire(LoginEvent.builder().action(LoginEvent.Action.LOGOUT).build()); + } + authenticationFacade.logout(servletRequest); + return null; + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthLoginPageBean.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthLoginPageBean.java new file mode 100644 index 0000000..e219c91 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthLoginPageBean.java @@ -0,0 +1,141 @@ +package de.cuioss.portal.ui.oauth; + +import static de.cuioss.tools.base.Preconditions.checkArgument; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.facade.AuthenticationResults; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.Oauth2AuthenticationFacade; +import de.cuioss.portal.authentication.oauth.Oauth2Configuration; +import de.cuioss.portal.ui.api.ui.context.CuiCurrentView; +import de.cuioss.portal.ui.api.ui.pages.HomePage; +import de.cuioss.portal.ui.runtime.page.AbstractLoginPageBean; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.uimodel.nameprovider.IDisplayNameProvider; +import de.cuioss.uimodel.result.ResultObject; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Page Bean for the Oauth2 Login Page Bean. Supports two mode:
      + *
        + *
      • Pseudo login page bean: The login page bean consists only of the view + * action directly redirecting either to the oauth server or into the + * application
      • + *
      • Landing page bean: A landing page with a button linking to the oauth + * server login page. When using deep linking, this page is skipped.
      • + *
      + * In both mode it is checked if page is accessed via redirect from oauth server + * with matching parameters.
      + * + * @author Matthias Walliczek + */ +@Named +@RequestScoped +@EqualsAndHashCode(callSuper = false) +@ToString +public class OauthLoginPageBean extends AbstractLoginPageBean { + + private static final CuiLogger log = new CuiLogger(OauthLoginPageBean.class); + + private static final long serialVersionUID = 5261664290647994366L; + + @Inject + private FacesContext facesContext; + + @Inject + @CuiCurrentView + private ViewDescriptor currentView; + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacade authenticationFacade; + + @Inject + @PortalWrappedOauthFacade + private WrappedOauthFacade wrappedOauthFacade; + + @Inject + private Oauth2Configuration oauth2Configuration; + + private boolean doRedirectOnFailure; + + /** + * Initialize the bean and check the configuration. + */ + @PostConstruct + public void init() { + if (null == oauth2Configuration) { + throw new IllegalStateException( + "Portal-528: Oauth2 configuration is invalid or missing, please fix the oauth2 configuration!"); + } + } + + /** + * Check if the client was redirected from an oauth2 server with valid code and + * login if so. Otherwise check if the client tries to access a deep link and + * skip this page if so. + * + * @return the next view after login if login was successful. Otherwise null. + */ + public String testLoginViewAction() { + var targetView = wrappedOauthFacade.retrieveTargetView(); + doRedirectOnFailure = !targetView.getViewId().equalsIgnoreCase(currentView.getLogicalViewId()) && !targetView + .getViewId().equalsIgnoreCase(NavigationUtils.lookUpToLogicalViewIdBy(facesContext, HomePage.OUTCOME)); + return loginAction(() -> targetView, ServletAdapterUtil.getRequest(facesContext), facesContext); + } + + /** + * Check if the client was redirected from an oauth2 server with valid code and + * login if so. Otherwise start the login implicit by redirecting to the oauth + * server. + * + * @return the next view after login if login was successful. Otherwise null. + */ + public String testLoginAndRedirectViewAction() { + var targetView = wrappedOauthFacade.retrieveTargetView(); + doRedirectOnFailure = true; + return loginAction(() -> targetView, ServletAdapterUtil.getRequest(facesContext), facesContext); + } + + private String loginTarget; + + /** + * @return The oauth2 login url. + */ + public String loginTarget() { + if (null == loginTarget) { + loginTarget = authenticationFacade.retrieveOauth2RedirectUrl(oauth2Configuration.getInitialScopes(), null); + } + return loginTarget; + } + + @Override + protected ResultObject doLogin(final HttpServletRequest currentServletRequest) { + checkArgument(null != currentView, "currentView must be available"); + return AuthenticationResults.validResult( + authenticationFacade.testLogin(currentView.getUrlParameter(), oauth2Configuration.getInitialScopes())); + } + + @Override + protected void handleLoginFailed(final IDisplayNameProvider message) { + if (doRedirectOnFailure) { + log.debug("login failed, redirecting to oauth server"); + wrappedOauthFacade.preserveCurrentView(); + doRedirectOnFailure = false; + authenticationFacade.sendRedirect(oauth2Configuration.getInitialScopes()); + } else { + log.debug("noop. redirectOnFailure is false"); + } + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthLogoutPageBean.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthLogoutPageBean.java new file mode 100644 index 0000000..78eff01 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthLogoutPageBean.java @@ -0,0 +1,141 @@ +package de.cuioss.portal.ui.oauth; + +import static de.cuioss.portal.configuration.OAuthConfigKeys.OPEN_ID_CLIENT_POST_LOGOUT_REDIRECT_URI; +import static de.cuioss.tools.string.MoreStrings.isBlank; +import static de.cuioss.tools.string.MoreStrings.isPresent; + +import java.util.Optional; + +import javax.annotation.Priority; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.event.Event; +import javax.enterprise.inject.Alternative; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.LoginEvent; +import de.cuioss.portal.authentication.PortalLoginEvent; +import de.cuioss.portal.authentication.PortalUser; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.authentication.oauth.Oauth2AuthenticationFacade; +import de.cuioss.portal.authentication.oauth.Oauth2Configuration; +import de.cuioss.portal.configuration.OAuthConfigKeys; +import de.cuioss.portal.configuration.common.PortalPriorities; +import de.cuioss.portal.ui.api.ui.pages.LoginPage; +import de.cuioss.portal.ui.api.ui.pages.LogoutPage; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogout; +import de.cuioss.tools.collect.CollectionBuilder; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.tools.net.UrlParameter; +import de.cuioss.tools.string.MoreStrings; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Oauth Logout Page Bean to redirect to the oauth server logout page. + * + * @author Matthias Walliczek + */ +@PortalCorePagesLogout +@Named(LogoutPage.BEAN_NAME) +@Alternative +@Priority(PortalPriorities.PORTAL_MODULE_LEVEL) +@RequestScoped +@EqualsAndHashCode +@ToString +public class OauthLogoutPageBean implements LogoutPage { + + private static final long serialVersionUID = -3588577094632702649L; + + private static final CuiLogger LOGGER = new CuiLogger(OauthLogoutPageBean.class); + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacade authenticationFacade; + + @Inject + private FacesContext facesContext; + + @Inject + @PortalLoginEvent + private Event preLogoutEvent; + + @Inject + @PortalUser + private AuthenticatedUserInfo authenticatedUserInfo; + + @Inject + private Oauth2Configuration oauth2Configuration; + + @Inject + @LoginPagePath + private String loginUrl; + + @Inject + private HttpServletRequest servletRequest; + + @Inject + private Oauth2Configuration configuration; + + /** + * Logs out and redirects to login page. + */ + @Override + public String logoutViewAction() { + if (MoreStrings.isEmpty(oauth2Configuration.getLogoutUri())) { + LOGGER.debug("no logout URI configured. redirecting to login page."); + performLogout(); + return LoginPage.OUTCOME; + } + + var idpLogoutUrl = authenticationFacade.retrieveClientLogoutUrl( + CollectionBuilder.copyFrom().add(getPostLogoutRedirectUriParam()).toImmutableSet()); + LOGGER.debug("Redirecting to IDP logout URL: {}", idpLogoutUrl); + + // Actually wrong if strictly sticking to rp-initiated-logout specification, + // but should not bother anyone either. + // If done right, the iframeLogout.xhtml should be called by IDP. + performLogout(); + + NavigationUtils.redirect(facesContext, idpLogoutUrl); + return null; + } + + @Override + public String performLogout() { + if (authenticatedUserInfo.isAuthenticated()) { + preLogoutEvent.fire(LoginEvent.builder().action(LoginEvent.Action.LOGOUT).build()); + } + return authenticationFacade.logout(ServletAdapterUtil.getRequest(facesContext)) ? LoginPage.OUTCOME : null; + } + + /** + * @return url parameter for post_logout_redirect_uri. the value is obtained + * from {@link Oauth2Configuration#getPostLogoutRedirectUri()} or, if + * not present, constructed from external context path and + * {@link #loginUrl}. + */ + private Optional getPostLogoutRedirectUriParam() { + + final var postLogoutRedirectUri = configuration.getPostLogoutRedirectUri(); + if (isPresent(postLogoutRedirectUri)) { + if (isBlank(configuration.getLogoutRedirectParamName())) { + LOGGER.warn("postLogoutRedirectUri set, but no url-parameter name. Set via: {}", + OAuthConfigKeys.OPEN_ID_CLIENT_LOGOUT_REDIRECT_PARAMETER); + return Optional.empty(); + } + + LOGGER.debug("postLogoutRedirectUri: {}", postLogoutRedirectUri); + return Optional.of(new UrlParameter(configuration.getLogoutRedirectParamName(), postLogoutRedirectUri)); + } + + LOGGER.debug("No postLogoutRedirectUri configured. Config-Key: {}", OPEN_ID_CLIENT_POST_LOGOUT_REDIRECT_URI); + return Optional.empty(); + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthMessagePhaseListener.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthMessagePhaseListener.java new file mode 100644 index 0000000..483d54c --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthMessagePhaseListener.java @@ -0,0 +1,78 @@ +package de.cuioss.portal.ui.oauth; + +import static de.cuioss.jsf.api.application.navigation.NavigationUtils.getCurrentView; +import static de.cuioss.jsf.api.servlet.ServletAdapterUtil.getResponse; +import static de.cuioss.portal.ui.oauth.WrappedOauthFacadeImpl.MESSAGES_IDENTIFIER; +import static javax.faces.event.PhaseId.RENDER_RESPONSE; + +import java.util.List; + +import javax.faces.application.FacesMessage; +import javax.faces.event.PhaseEvent; +import javax.faces.event.PhaseId; +import javax.faces.event.PhaseListener; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; + +import org.apache.deltaspike.jsf.api.listener.phase.JsfPhaseListener; + +import de.cuioss.jsf.api.common.util.CheckContextState; +import de.cuioss.tools.logging.CuiLogger; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Restore {@link FacesMessage}s after a redirect to the oauth server and back + * again. Before redirecting to the oauth server + * {@link WrappedOauthFacadeImpl#preserveCurrentView(HttpServletRequest)} will + * store the existing messages in the messing, and this class will restore them + * before {@link PhaseId#RENDER_RESPONSE}. + */ +@JsfPhaseListener +@EqualsAndHashCode +@ToString +public class OauthMessagePhaseListener implements PhaseListener { + + private static final long serialVersionUID = 837984685534479200L; + + private static final CuiLogger log = new CuiLogger(OauthMessagePhaseListener.class); + + @Inject + private Provider servletRequestProvider; + + @Override + public void afterPhase(PhaseEvent event) { + // NOP + } + + @Override + public void beforePhase(PhaseEvent event) { + var context = event.getFacesContext(); + final var response = getResponse(context); + if (log.isTraceEnabled()) { + log.trace("currentView: {}", getCurrentView(context)); + log.trace("responseComplete: {}", context.getResponseComplete()); + log.trace("released: {}", context.isReleased()); + log.trace("postback: {}", context.isPostback()); + log.trace("committed: {}", response.isCommitted()); + } + if (CheckContextState.isResponseNotComplete(context) && !response.isCommitted() + && null != servletRequestProvider.get().getSession(false) + && null != servletRequestProvider.get().getSession().getAttribute(MESSAGES_IDENTIFIER)) { + var messages = (List) servletRequestProvider.get().getSession() + .getAttribute(MESSAGES_IDENTIFIER); + log.trace("restore messages: {}", messages); + messages.forEach(message -> event.getFacesContext().addMessage(null, + // because the old message may already be rendered (and the rendered flag was + // set) we need to reset it + new FacesMessage(message.getSeverity(), message.getSummary(), message.getDetail()))); + servletRequestProvider.get().getSession().removeAttribute(MESSAGES_IDENTIFIER); + } + } + + @Override + public PhaseId getPhaseId() { + return RENDER_RESPONSE; + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthMissingScopesErrorHandler.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthMissingScopesErrorHandler.java new file mode 100644 index 0000000..c6fcafb --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthMissingScopesErrorHandler.java @@ -0,0 +1,34 @@ +package de.cuioss.portal.ui.oauth; + +import java.util.Collections; + +import de.cuioss.jsf.api.components.model.resultContent.ErrorController; +import de.cuioss.portal.ui.api.ui.lazyloading.LazyLoadingErrorHandler; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.uimodel.result.ResultDetail; +import de.cuioss.uimodel.result.ResultState; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class OauthMissingScopesErrorHandler extends LazyLoadingErrorHandler { + + private final WrappedOauthFacade wrappedOauthFacade; + + @Override + public void handleResultDetail(final ResultState state, final ResultDetail detail, final Enum errorCode, + final ErrorController errorController, final CuiLogger log) { + + log.trace("OauthMissingScopesErrorHandler handleRequestError"); + + if (null != detail && detail.getCause().isPresent()) { + @SuppressWarnings("squid:S3655") // isPresent is called well + final var cause = detail.getCause().get(); + if (cause instanceof MissingScopesException) { + wrappedOauthFacade.handleMissingScopesException((MissingScopesException) cause, Collections.emptyMap()); + } else { + super.handleRequestError(cause, detail.getDetail().toString(), errorController, log); + } + } + } + +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthRenewComponent.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthRenewComponent.java new file mode 100644 index 0000000..bac4335 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/OauthRenewComponent.java @@ -0,0 +1,51 @@ +package de.cuioss.portal.ui.oauth; + +import java.util.Optional; + +import javax.faces.component.FacesComponent; +import javax.faces.component.UINamingContainer; + +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.Oauth2AuthenticationFacade; +import de.cuioss.portal.core.cdi.PortalBeanManager; +import de.cuioss.tools.logging.CuiLogger; +import lombok.Getter; + +/** + * Backing bean for sso composite component. + * + * @author Matthias Walliczek + */ +@FacesComponent("de.cuioss.portal.ui.oauth.OauthRenewComponent") +public class OauthRenewComponent extends UINamingContainer { + + private static final CuiLogger log = new CuiLogger(OauthRenewComponent.class); + + @Getter + private String loginUrl; + + @Getter + private final String renewUrl; + + @Getter + private final String renewInterval; + + /** + * initialize + */ + public OauthRenewComponent() { + final Optional authenticationFacade = PortalBeanManager + .resolveBean(Oauth2AuthenticationFacade.class, PortalAuthenticationFacade.class); + if (authenticationFacade.isPresent()) { + renewUrl = authenticationFacade.get().retrieveOauth2RenewUrl(); + renewInterval = authenticationFacade.get().retrieveRenewInterval(); + loginUrl = authenticationFacade.get().getLoginUrl(); + } else { + log.error(PortalBeanManager.createLogMessage(Oauth2AuthenticationFacade.class, + PortalAuthenticationFacade.class)); + renewUrl = null; + renewInterval = null; + loginUrl = null; + } + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/PortalWrappedOauthFacade.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/PortalWrappedOauthFacade.java new file mode 100644 index 0000000..882d746 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/PortalWrappedOauthFacade.java @@ -0,0 +1,23 @@ +package de.cuioss.portal.ui.oauth; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Defines Instances of {@link WrappedOauthFacade} + * + * @author Matthias Walliczek + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface PortalWrappedOauthFacade { +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/RedirectorImpl.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/RedirectorImpl.java new file mode 100644 index 0000000..0ff35a6 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/RedirectorImpl.java @@ -0,0 +1,25 @@ +package de.cuioss.portal.ui.oauth; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Provider; + +import de.cuioss.jsf.api.application.navigation.NavigationUtils; +import de.cuioss.portal.authentication.oauth.OauthRedirector; + +/** + * Implementation of the {@link OauthRedirector} interface using + * {@link NavigationUtils#redirect(FacesContext, String)}. + */ +@ApplicationScoped +public class RedirectorImpl implements OauthRedirector { + + @Inject + private Provider facesContextProvider; + + @Override + public void sendRedirect(String url) throws IllegalStateException { + NavigationUtils.redirect(facesContextProvider.get(), url); + } +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/WrappedOauthFacade.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/WrappedOauthFacade.java new file mode 100644 index 0000000..d4e39d3 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/WrappedOauthFacade.java @@ -0,0 +1,83 @@ +package de.cuioss.portal.ui.oauth; + +import java.io.Serializable; +import java.util.Map; + +import de.cuioss.jsf.api.application.navigation.ViewIdentifier; +import de.cuioss.portal.authentication.oauth.Oauth2AuthenticationFacade; + +/** + * Wrapper for the {@link Oauth2AuthenticationFacade} using JSF specific + * providers and provide simple methods to the user. + * + * @author Matthias Walliczek + */ +public interface WrappedOauthFacade { + + /** + * Retrieve a token for the default scopes. May cause a redirect to the oauth + * server. + * + * @return the accessToken is available, otherwise null is returned, a redirect + * to the oauth server is issued, and the page is accessed a second + * time. + */ + String retrieveToken(); + + /** + * Retrieve a token for the given scopes. May cause a redirect to the oauth + * server. + * + * @param scopes the scopes as space separated list. + * @return the accessToken is available, otherwise null is returned, a redirect + * to the oauth server is issued, and the page is accessed a second + * time. + */ + String retrieveToken(String scopes); + + /** + * Handles a {@link MissingScopesException} by retrieving the missing scopes and + * triggering a redirect to the oauth server if necessary. + * + * @param e The exception to handle + * @param viewParameters A map of view parameters to keep during the oauth + * redirect + */ + void handleMissingScopesException(MissingScopesException e, Map viewParameters); + + /** + * Handles a {@link MissingScopesException} by retrieving the missing scopes and + * triggering a redirect to the oauth server if necessary. + * + * @param e The exception to handle + * @param initialScopes The initial scopes to be added to the missing scopes + * @param viewParameters A map of view parameters to keep during the oauth + * redirect + */ + void handleMissingScopesException(MissingScopesException e, String initialScopes, + Map viewParameters); + + /** + * Retrieve the view parameters stored by + * {@link #handleMissingScopesException(MissingScopesException, Map)} after the + * redirect to the oauth server + * + * @return a map of view parameters + */ + Map retrieveViewParameters(); + + /** + * Retrieve the target view from the history manager. Because of problems with + * the window scope after redirect from oauth server use a session stored target + * view if present. + * + * @return + */ + ViewIdentifier retrieveTargetView(); + + /** + * Store the target view in the session if not already present to handle deep + * linking + */ + void preserveCurrentView(); +} diff --git a/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/WrappedOauthFacadeImpl.java b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/WrappedOauthFacadeImpl.java new file mode 100644 index 0000000..4acbe2b --- /dev/null +++ b/modules/portal-ui-oauth/src/main/java/de/cuioss/portal/ui/oauth/WrappedOauthFacadeImpl.java @@ -0,0 +1,172 @@ +package de.cuioss.portal.ui.oauth; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; + +import de.cuioss.jsf.api.application.history.HistoryManager; +import de.cuioss.jsf.api.application.navigation.ViewIdentifier; +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.Oauth2AuthenticationFacade; +import de.cuioss.portal.authentication.oauth.Oauth2Configuration; +import de.cuioss.portal.ui.api.history.PortalHistoryManager; +import de.cuioss.portal.ui.api.ui.context.CuiCurrentView; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.tools.string.Joiner; + +/** + * Implementation of {@link WrappedOauthFacade} using {@link HistoryManager} and + * {@link HttpServletRequest} to handle redirects to / from oauth server and + * storage of view parameters and requested views. + * + * @author Matthias Walliczek + */ +@ApplicationScoped +@PortalWrappedOauthFacade +public class WrappedOauthFacadeImpl implements WrappedOauthFacade { + + private static final CuiLogger log = new CuiLogger(WrappedOauthFacadeImpl.class); + + /** + * Attribute name for the target view id to be stored in the session. + */ + private static final String VIEW_IDENTIFIER = "oauthViewIdentifier"; + + private static final String PARAMETER_IDENTIFIER = "oauthViewparameter"; + + static final String MESSAGES_IDENTIFIER = "oauthMessages"; + + private static final String MESSAGE_GET_ATTRIBUTE_FAILED = "session.getAttribute failed"; + + @Inject + private Provider servletRequestProvider; + + @Inject + private Provider facesContextProvider; + + @Inject + @CuiCurrentView + private Provider currentViewProvider; + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacade authenticationFacade; + + @Inject + private Provider oauth2ConfigurationProvider; + + @Inject + @PortalHistoryManager + private Provider historyManagerProvider; + + @Override + public String retrieveToken() { + return retrieveToken(oauth2ConfigurationProvider.get().getInitialScopes()); + } + + @Override + public String retrieveToken(final String scopes) { + log.trace("retrieveToken for scopes: {}", scopes); + var currentView = currentViewProvider.get(); + var request = servletRequestProvider.get(); + var token = authenticationFacade.retrieveToken(scopes); + if (null == token) { + historyManagerProvider.get().addCurrentUriToHistory(currentView); + // store the current view in the session to handle problems with window scope + // after + // redirect from oauth server + preserveCurrentView(request); + } + return token; + } + + @Override + public void handleMissingScopesException(MissingScopesException e, Map parameters) { + log.trace("handleMissingScopesException", e); + handleMissingScopesException(e, oauth2ConfigurationProvider.get().getInitialScopes(), parameters); + } + + @Override + public void handleMissingScopesException(MissingScopesException e, String initialScopes, + Map parameters) { + log.trace(e, "handleMissingScopesException {}", initialScopes); + var request = servletRequestProvider.get(); + request.getSession().setAttribute(PARAMETER_IDENTIFIER, new HashMap<>(parameters)); + retrieveToken(Joiner.on(' ').join(initialScopes, e.getMissingScopes())); + } + + @Override + public Map retrieveViewParameters() { + log.trace("retrieveViewParameters"); + Map result = new HashMap<>(); + var servletRequest = servletRequestProvider.get(); + try { + if (null != servletRequest.getSession(false) + && null != servletRequest.getSession().getAttribute(PARAMETER_IDENTIFIER)) { + result = (Map) servletRequest.getSession().getAttribute(PARAMETER_IDENTIFIER); + servletRequest.getSession().removeAttribute(PARAMETER_IDENTIFIER); + if (null != servletRequest.getSession().getAttribute(MESSAGES_IDENTIFIER)) { + var messages = (List) servletRequest.getSession() + .getAttribute(MESSAGES_IDENTIFIER); + log.trace("restore messages: {}", messages); + messages.forEach(message -> facesContextProvider.get().addMessage(null, message)); + servletRequest.getSession().removeAttribute(MESSAGES_IDENTIFIER); + } + } + } catch (IllegalStateException e) { + log.debug(MESSAGE_GET_ATTRIBUTE_FAILED, e); + } + return result; + } + + @Override + public ViewIdentifier retrieveTargetView() { + var targetView = historyManagerProvider.get().getCurrentView(); + log.trace("retrieveTargetView from historyManager.getCurrentView(): {}", targetView); + var servletRequest = servletRequestProvider.get(); + try { + if (null != servletRequest.getSession(false) + && null != servletRequest.getSession().getAttribute(VIEW_IDENTIFIER)) { + targetView = (ViewIdentifier) servletRequest.getSession().getAttribute(VIEW_IDENTIFIER); + log.trace("retrieveTargetView servletRequest.getSession().getAttribute(VIEW_IDENTIFIER): {}", + targetView); + servletRequest.getSession().setAttribute(VIEW_IDENTIFIER, null); + log.trace("retrieveTargetView servletRequest VIEW_IDENTIFIER reset"); + } + } catch (IllegalStateException e) { + log.debug(MESSAGE_GET_ATTRIBUTE_FAILED, e); + } + return targetView; + } + + @Override + public void preserveCurrentView() { + log.trace("preserveCurrentView"); + var servletRequest = servletRequestProvider.get(); + try { + if (null != servletRequest.getSession(false) + && null == servletRequest.getSession(false).getAttribute(VIEW_IDENTIFIER)) { + preserveCurrentView(servletRequest); + } + } catch (IllegalStateException e) { + log.debug(MESSAGE_GET_ATTRIBUTE_FAILED, e); + } + } + + private void preserveCurrentView(HttpServletRequest servletRequest) { + log.debug("preserveCurrentView Preserving target {}", historyManagerProvider.get().getCurrentView()); + servletRequest.getSession().setAttribute(VIEW_IDENTIFIER, historyManagerProvider.get().getCurrentView()); + var messages = facesContextProvider.get().getMessageList(null); + log.trace("preserveCurrentView store messages: {}", messages); + servletRequest.getSession().setAttribute(MESSAGES_IDENTIFIER, messages); + } +} diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/beans.xml b/modules/portal-ui-oauth/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..d54a394 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/faces-config.xml b/modules/portal-ui-oauth/src/main/resources/META-INF/faces-config.xml new file mode 100644 index 0000000..d02fb75 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/faces-config.xml @@ -0,0 +1,33 @@ + + + CuiPortalOauth + + * + + login + /faces/guest/login.xhtml + + + + + + * + + logout + /faces/guest/logout.xhtml + + + + + + * + + oauth-error + /faces/guest/oauth-error.xhtml + + + + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/iframeLogout.xhtml b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/iframeLogout.xhtml new file mode 100644 index 0000000..4e14758 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/iframeLogout.xhtml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/login.xhtml b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/login.xhtml new file mode 100644 index 0000000..7de2713 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/login.xhtml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + #{msgs['page.login.title']} + + + + + + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/logout.xhtml b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/logout.xhtml new file mode 100644 index 0000000..e406e4d --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/logout.xhtml @@ -0,0 +1,37 @@ + + + + + + + + + + + + #{msgs['page.logout.title']} + + + +
      +
      +

      #{msgs['page.logout.srHeader']}

      +
      +

      + +

      +

      + +

      +
      +
      +
      +
      +
      + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/oauth-error.xhtml b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/oauth-error.xhtml new file mode 100644 index 0000000..06f149f --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/faces/guest/oauth-error.xhtml @@ -0,0 +1,42 @@ + + + + + + #{msgs['page.error.oauth.title']} + + +

      #{msgs['page.error.oauth.srHeader']}

      +

      #{msgs['page.error.oauth.title']}

      + +

      + +

      + +

      + +

      +
      +
      + + +

      + +

      +
      +

      + + + +

      +
      +
      + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/resources/de.cuioss.portal.ui.oauth/login_page.css b/modules/portal-ui-oauth/src/main/resources/META-INF/resources/de.cuioss.portal.ui.oauth/login_page.css new file mode 100644 index 0000000..1700255 --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/resources/de.cuioss.portal.ui.oauth/login_page.css @@ -0,0 +1,141 @@ +.guest_login { + min-height: 450px; +} + +.login-wrapper .container { + max-width: 340px; +} + +.product-name, .module-name { + color: white; + font-weight: bold; + text-align: center; +} + +.form-login { + max-width: 320px; + color: white; + background-color: rgba(80, 80, 80, 0.8); + padding: 15px 15px 30px; +} + +.guest_login .navbar { + display: none; +} + +/* adapt layout of sticky-messages on login page */ +.login-wrapper .sticky-messages .single-sticky-message { + margin-bottom: 0; + background-color: rgba(255, 255, 255, 0.7); +} + +.login-wrapper .sticky-messages { + position: absolute; + top: 0; + width: 100%; + z-index: 1000; +} + +.login-wrapper .sticky-messages + .container .logo { + display: none; +} + +@media screen and (min-height: 600px) and (max-height: 720px) { + .form-login { + position: fixed; + width: 100%; + bottom: 5%; + } +} + +@media screen and (max-height: 599px) { + .logo { + display: none; + } +} + +/* xs media definition */ +@media screen and (min-width: 350px) and (max-width: 767px) { + .login-wrapper .container { + width: 95%; + } +} + +/* xxs media definition */ +@media screen and (min-width: 200px) and (max-width: 349px) { + .login-wrapper .container { + width: 95%; + margin-right: 1%; + } + + .form-login { + width: 100%; + } + + .login-wrapper + .page-footer { + background-color: rgba(80, 80, 80, 0.8); + color: white; + min-height: 25px; + } + + .login-wrapper + .page-footer p { + margin-top: 5px; + margin-bottom: 5px; + } +} + +/* mobile device adaptation */ +@media screen and (orientation: landscape) and (max-width: 767px) { + .login-wrapper .container { + padding-top: 10px; + } + + .login-wrapper .logo { + display: none; + } + + .form-login { + padding-bottom: 15px; + } + + .form-login .product-name { + margin-top: 0; + font-size: 16px; + } + + .form-login .module-name { + margin-bottom: 10px; + margin-top: 10px; + font-size: 14px; + } + + .form-login .form-group { + margin-bottom: 10px; + } + + .form-login hr ~ a:before { + clear: both; + } + + .form-login hr ~ a:last-of-type { + margin-top: 0; + } + + .form-login hr ~ a.btn-block { + width: 49%; + display: inline-block; + position: static; + } + + .login-wrapper + .page-footer { + background-color: rgba(80, 80, 80, 0.8); + color: white; + min-height: 25px; + } + + .login-wrapper + .page-footer p { + margin-top: 5px; + margin-bottom: 5px; + } +} + diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/resources/js/sso.js b/modules/portal-ui-oauth/src/main/resources/META-INF/resources/js/sso.js new file mode 100644 index 0000000..2de2c3e --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/resources/js/sso.js @@ -0,0 +1,69 @@ +/*function initTokenRenew() { + if ($("form.cuiOauthForm").data("timeout") >= 0) { + $("form.cuiOauthForm").data("timeoutCallback", setTimeout(function () { + $.ajax({ + url: $("form.cuiOauthForm").data("uri"), + xhrFields: { + withCredentials: true + } + }) + .always(function () { reloadTokenForm(); }) + }, $("form.cuiOauthForm").data("timeout") * 1000)); + } else if ($("form.cuiOauthForm").data("timeout") < 0 && $("form.cuiOauthForm").data("uri")) { + location.href = $("form.cuiOauthForm").data("uri"); + } +}*/ + +function initTokenRenew() { + if ($("form.cuiOauthForm").data("timeout") >= 0) { + $("form.cuiOauthForm").data("timeoutCallback", setTimeout(function () { + $.ajax({ + url: $("form.cuiOauthForm").data("uri"), + xhrFields: { + withCredentials: true + }, + success: function (data) { + parseResponse(data); + }, + error: function () { + location.href = $("form.cuiOauthForm").data("uri"); + } + }) + + }, $("form.cuiOauthForm").data("timeout") * 1000)); + } else if ($("form.cuiOauthForm").data("timeout") < 0 && $("form.cuiOauthForm").data("uri")) { + location.href = $("form.cuiOauthForm").data("uri"); + } +} + +function parseResponse(data) { + var code = data.code; + var state = data.state; + if (null != code && null != state) { + var loginUrl = $("form.cuiOauthForm").data("login") + if (null != loginUrl) { + var origin = window.location.origin; + var path = window.location.pathname; + var product = path.split("/")[1]; + var loginRedirect = origin+"/"+product+loginUrl+"?code=" + code + "&state=" + state; + $.ajax({ + url: loginRedirect, + xhrFields: { + withCredentials: true + } + }) + .always(function () { reloadTokenForm(); }) + } + } +} + +function stopRenew() { + if ($("form.cuiOauthForm").data("timeoutCallback")) { + clearTimeout($("form.cuiOauthForm").data("timeoutCallback")); + } +} + +$(function () { + initTokenRenew(); + icw.cui.registerOnIdle(stopRenew) +}); diff --git a/modules/portal-ui-oauth/src/main/resources/META-INF/resources/oauth/sso.xhtml b/modules/portal-ui-oauth/src/main/resources/META-INF/resources/oauth/sso.xhtml new file mode 100644 index 0000000..69f6d3b --- /dev/null +++ b/modules/portal-ui-oauth/src/main/resources/META-INF/resources/oauth/sso.xhtml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/MissingScopesErrorDecoderTest.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/MissingScopesErrorDecoderTest.java new file mode 100644 index 0000000..d44d84a --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/MissingScopesErrorDecoderTest.java @@ -0,0 +1,58 @@ +package de.cuioss.portal.ui.oauth; + +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import java.net.URI; +import java.net.URISyntaxException; + +import javax.ws.rs.core.Response; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MissingScopesErrorDecoderTest { + + @Test + void testUpperCaseHeader() throws URISyntaxException { + final var response = Response.created(new URI("http://localhost")).status(SC_FORBIDDEN, "forbidden") + .header("WWW-Authenticate", "error=\"insufficient_scope\", scope=\"abc\"").build(); + var msed = new MissingScopesErrorDecoder(); + Assertions.assertTrue(msed.handles(SC_FORBIDDEN, response.getHeaders())); + var result = msed.toThrowable(response); + Assertions.assertNotNull(result); + Assertions.assertEquals("abc", result.getMissingScopes()); + } + + @Test + void testBearerError() throws URISyntaxException { + final var response = Response.created(new URI("http://localhost")).status(SC_FORBIDDEN, "forbidden") + .header("WWW-Authenticate", "Bearer error=\"insufficient_scope\", scope=\"abc\"").build(); + var msed = new MissingScopesErrorDecoder(); + Assertions.assertTrue(msed.handles(SC_FORBIDDEN, response.getHeaders())); + var result = msed.toThrowable(response); + Assertions.assertNotNull(result); + Assertions.assertEquals("abc", result.getMissingScopes()); + } + + @Test + void testLowerCaseHeader() throws URISyntaxException { + final var response = Response.created(new URI("http://localhost")).status(SC_FORBIDDEN, "forbidden") + .header("www-authenticate", "error=\"insufficient_scope\", scope=\"abc\"").build(); + var msed = new MissingScopesErrorDecoder(); + Assertions.assertTrue(msed.handles(SC_FORBIDDEN, response.getHeaders())); + var result = msed.toThrowable(response); + Assertions.assertNotNull(result); + Assertions.assertEquals("abc", result.getMissingScopes()); + } + + @Test + void test404() throws URISyntaxException { + final var response = Response.created(new URI("http://localhost")).status(SC_NOT_FOUND, "not found") + .build(); + var msed = new MissingScopesErrorDecoder(); + Assertions.assertFalse(msed.handles(SC_NOT_FOUND, response.getHeaders())); + var result = msed.toThrowable(response); + Assertions.assertNull(result); + } +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/ModuleConsistencyTest.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/ModuleConsistencyTest.java new file mode 100644 index 0000000..ab94f1c --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/ModuleConsistencyTest.java @@ -0,0 +1,14 @@ +package de.cuioss.portal.ui.oauth; + +import org.jboss.weld.environment.se.Weld; + +import de.cuioss.portal.core.test.tests.BaseModuleConsistencyTest; +import de.cuioss.test.jsf.producer.JsfObjectsProducers; +import de.cuioss.test.jsf.producer.ServletObjectsFromJSFContextProducers; + +class ModuleConsistencyTest extends BaseModuleConsistencyTest { + @Override + protected Weld modifyWeldContainer(Weld weld) { + return weld.addBeanClasses(ServletObjectsFromJSFContextProducers.class, JsfObjectsProducers.class); + } +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/Oauth2AuthenticationFacadeMock.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/Oauth2AuthenticationFacadeMock.java new file mode 100644 index 0000000..692ac96 --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/Oauth2AuthenticationFacadeMock.java @@ -0,0 +1,178 @@ +package de.cuioss.portal.ui.oauth; + +import static de.cuioss.tools.net.UrlParameter.createParameterString; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; + +import de.cuioss.portal.authentication.AuthenticatedUserInfo; +import de.cuioss.portal.authentication.facade.AuthenticationSource; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.model.BaseAuthenticatedUserInfo; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.authentication.oauth.Oauth2AuthenticationFacade; +import de.cuioss.portal.configuration.common.PortalPriorities; +import de.cuioss.tools.logging.CuiLogger; +import de.cuioss.tools.net.UrlParameter; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@SuppressWarnings("javadoc") +@PortalAuthenticationFacade +@ApplicationScoped +@EqualsAndHashCode +@ToString +@Alternative +@Priority(PortalPriorities.PORTAL_ASSEMBLY_LEVEL) +public class Oauth2AuthenticationFacadeMock implements Oauth2AuthenticationFacade { + + private static final CuiLogger log = new CuiLogger(Oauth2AuthenticationFacadeMock.class); + + @Getter + @Setter + private boolean authenticated = true; + + @Getter + private String redirectUrl; + + @Setter + private String tokenToRetrieve = "token"; + + @Setter + private String renewUrl; + + @Setter + private String renewInterval; + + @Setter + private String clientLogoutUrl; + + @Inject + @LoginPagePath + private String loginUrl; + + @Inject + private Provider servletRequestProvider; + + public void resetRedirectUrl() { + redirectUrl = null; + } + + @Override + public AuthenticatedUserInfo testLogin(final List parameters, final String scopes) { + AuthenticatedUserInfo currentUser = null; + var servletRequest = servletRequestProvider.get(); + try { + if (null != servletRequest.getSession(false)) { + currentUser = (AuthenticatedUserInfo) servletRequest.getSession().getAttribute("AuthenticatedUserInfo"); + } + } catch (IllegalStateException e) { + log.debug("servletRequest.getSession().getAttribute failed", e); + } + if ((null == currentUser || !currentUser.isAuthenticated()) && authenticated) { + servletRequest.getSession().invalidate(); + } + return retrieveCurrentAuthenticationContext(servletRequest); + } + + @Override + public void invalidateToken() { + + } + + @Override + public boolean logout(final HttpServletRequest servletRequest) { + // TODO Auto-generated method stub + return false; + } + + @Override + public AuthenticatedUserInfo retrieveCurrentAuthenticationContext(final HttpServletRequest servletRequest) { + var authenticatedUserInfo = BaseAuthenticatedUserInfo.builder() + .authenticated(authenticated).identifier("user").qualifiedIdentifier("user").displayName("user") + .build(); + try { + servletRequest.getSession().setAttribute("AuthenticatedUserInfo", authenticatedUserInfo); + } catch (IllegalStateException e) { + log.debug(".getSession().setAttribute failed", e); + } + return authenticatedUserInfo; + + } + + @Override + public String retrieveToken(final String scopes) { + return tokenToRetrieve; + } + + @Override + public String retrieveToken(final AuthenticatedUserInfo currentUser, final String scopes) { + return tokenToRetrieve; + } + + @Override + public Map retrieveIdToken(final AuthenticatedUserInfo currentUser) { + return Collections.emptyMap(); + } + + @Override + public String retrieveClientToken(final String scopes) { + return null; + } + + @Override + public void sendRedirect(final String scopes) { + redirectUrl = loginUrl; + } + + @Override + public String retrieveOauth2RedirectUrl(final String scopes, final String idToken) { + return "redirect"; + } + + @Override + public String retrieveOauth2RenewUrl() { + return renewUrl; + } + + @Override + public String retrieveRenewInterval() { + return renewInterval; + } + + @Override + public AuthenticatedUserInfo refreshUserinfo() { + return null; + } + + @Override + public String getLoginUrl() { + return loginUrl; + } + + @Override + public String retrieveClientLogoutUrl(Set additionalUrlParams) { + return clientLogoutUrl + createParameterString(additionalUrlParams.toArray(UrlParameter[]::new)); + } + + @Override + public void sendRedirect() { + redirectUrl = loginUrl; + } + + @Override + public AuthenticationSource getAuthenticationSource() { + return AuthenticationSource.OPEN_ID_CONNECT; + } +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/Oauth2ConfigurationProducerMock.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/Oauth2ConfigurationProducerMock.java new file mode 100644 index 0000000..05dddbb --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/Oauth2ConfigurationProducerMock.java @@ -0,0 +1,27 @@ +package de.cuioss.portal.ui.oauth; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; + +import de.cuioss.portal.authentication.oauth.Oauth2Configuration; +import de.cuioss.portal.authentication.oauth.impl.Oauth2ConfigurationImpl; +import de.cuioss.portal.configuration.common.PortalPriorities; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ApplicationScoped +@EqualsAndHashCode +@ToString +@Alternative +@Priority(PortalPriorities.PORTAL_ASSEMBLY_LEVEL) +public class Oauth2ConfigurationProducerMock { + + @Produces + @Setter + @Getter + private Oauth2Configuration configuration = new Oauth2ConfigurationImpl(); +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthLoginPageBeanTest.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthLoginPageBeanTest.java new file mode 100644 index 0000000..44a25e3 --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthLoginPageBeanTest.java @@ -0,0 +1,130 @@ +package de.cuioss.portal.ui.oauth; + +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.DESCRIPTOR_HOME; +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.DESCRIPTOR_LOGIN; +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.DESCRIPTOR_PREFERENCES; +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.VIEW_PREFERENCES_LOGICAL_VIEW_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; + +import org.jboss.weld.exceptions.WeldException; +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.jboss.weld.junit5.auto.EnableAlternatives; +import org.junit.jupiter.api.Test; + +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.configuration.PortalConfigurationSource; +import de.cuioss.portal.core.test.mocks.configuration.PortalTestConfiguration; +import de.cuioss.portal.ui.api.history.PortalHistoryManager; +import de.cuioss.portal.ui.api.ui.context.CuiCurrentView; +import de.cuioss.portal.ui.runtime.application.view.HttpHeaderFilterImpl; +import de.cuioss.portal.ui.runtime.application.view.matcher.ViewMatcherProducer; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.mocks.PortalHistoryManagerMock; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import de.cuioss.test.jsf.producer.ServletObjectsFromJSFContextProducers; +import lombok.Getter; + +@EnablePortalUiEnvironment +@AddBeanClasses({ Oauth2AuthenticationFacadeMock.class, WrappedOauthFacadeImpl.class, HttpHeaderFilterImpl.class, + ViewMatcherProducer.class, Oauth2ConfigurationProducerMock.class, ServletObjectsFromJSFContextProducers.class }) +@EnableAlternatives(OauthLoginPageBeanTest.class) +class OauthLoginPageBeanTest extends AbstractPageBeanTest { + + @Inject + @Getter + private OauthLoginPageBean underTest; + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacadeMock oauth2AuthenticationFacadeMock; + + @Inject + private Oauth2ConfigurationProducerMock oauth2ConfigurationProducerMock; + + @Inject + @PortalHistoryManager + private PortalHistoryManagerMock portalHistoryManagerMock; + + @Produces + @CuiCurrentView + @RequestScoped + @Alternative + ViewDescriptor getCurrentView() { + return DESCRIPTOR_LOGIN; + } + + @Inject + @PortalConfigurationSource + private PortalTestConfiguration configuration; + + @Produces + @LoginPagePath + private String loginUrl = "login.jsf"; + + @Test + void testUnauthorizedShouldCauseRedirect() { + oauth2AuthenticationFacadeMock.setAuthenticated(false); + var result = underTest.testLoginAndRedirectViewAction(); + assertNull(result); + assertEquals("login.jsf", oauth2AuthenticationFacadeMock.getRedirectUrl()); + } + + @Test + void testAuthenticatedShouldRedirectCorrect() { + portalHistoryManagerMock.addCurrentUriToHistory(DESCRIPTOR_PREFERENCES); + oauth2AuthenticationFacadeMock.setAuthenticated(false); + underTest.testLoginAndRedirectViewAction(); + assertNotNull(oauth2AuthenticationFacadeMock.getRedirectUrl()); + oauth2AuthenticationFacadeMock.resetRedirectUrl(); + oauth2AuthenticationFacadeMock.setAuthenticated(true); + var result = underTest.testLoginAndRedirectViewAction(); + assertNull(result); + assertNull(oauth2AuthenticationFacadeMock.getRedirectUrl()); + assertRedirect(VIEW_PREFERENCES_LOGICAL_VIEW_ID); + } + + @Test + void testAuthenticatedShouldRedirectCorrectAtDeepLinkingAndLandingPage() { + portalHistoryManagerMock.addCurrentUriToHistory(DESCRIPTOR_PREFERENCES); + oauth2AuthenticationFacadeMock.setAuthenticated(false); + underTest.testLoginViewAction(); + assertNotNull(oauth2AuthenticationFacadeMock.getRedirectUrl()); + oauth2AuthenticationFacadeMock.resetRedirectUrl(); + oauth2AuthenticationFacadeMock.setAuthenticated(true); + var result = underTest.testLoginViewAction(); + assertNull(result); + assertNull(oauth2AuthenticationFacadeMock.getRedirectUrl()); + assertRedirect(VIEW_PREFERENCES_LOGICAL_VIEW_ID); + } + + @Test + void testAuthenticatedShouldRedirectCorrectAtLandingPage() { + portalHistoryManagerMock.addCurrentUriToHistory(DESCRIPTOR_HOME); + oauth2AuthenticationFacadeMock.setAuthenticated(false); + underTest.testLoginViewAction(); + assertNull(oauth2AuthenticationFacadeMock.getRedirectUrl()); + } + + @Test + void testLoginTarget() { + assertNotNull(underTest.loginTarget()); + } + + @Test + void testNoConfig() { + oauth2ConfigurationProducerMock.setConfiguration(null); + assertThrows(WeldException.class, () -> { + underTest.testLoginViewAction(); + }); + } +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthLogoutPageBeanTest.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthLogoutPageBeanTest.java new file mode 100644 index 0000000..c4eb714 --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthLogoutPageBeanTest.java @@ -0,0 +1,81 @@ +package de.cuioss.portal.ui.oauth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import javax.enterprise.inject.Produces; +import javax.inject.Inject; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.authentication.oauth.impl.Oauth2ConfigurationImpl; +import de.cuioss.portal.configuration.PortalConfigurationKeys; +import de.cuioss.portal.configuration.PortalConfigurationSource; +import de.cuioss.portal.core.test.mocks.authentication.PortalTestUserProducer; +import de.cuioss.portal.core.test.mocks.configuration.PortalTestConfiguration; +import de.cuioss.portal.ui.api.ui.pages.PortalCorePagesLogout; +import de.cuioss.portal.ui.runtime.application.view.matcher.ViewMatcherProducer; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.tests.AbstractPageBeanTest; +import de.cuioss.test.jsf.producer.ServletObjectsFromJSFContextProducers; +import lombok.Getter; + +@EnablePortalUiEnvironment +@AddBeanClasses({ Oauth2AuthenticationFacadeMock.class, ViewMatcherProducer.class, + Oauth2ConfigurationProducerMock.class, PortalTestUserProducer.class, + ServletObjectsFromJSFContextProducers.class }) +class OauthLogoutPageBeanTest extends AbstractPageBeanTest { + + @Inject + @PortalCorePagesLogout + @Getter + private OauthLogoutPageBean underTest; + + @Produces + @LoginPagePath + private String loginUrl = "login.jsf"; + + @Inject + @PortalConfigurationSource + private PortalTestConfiguration configuration; + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacadeMock facadeMock; + + @Inject + private Oauth2ConfigurationProducerMock oAuthConfiguration; + + @BeforeEach + void beforeTest() { + configuration.put(PortalConfigurationKeys.PORTAL_SESSION_TIMEOUT, "20"); + configuration.fireEvent(); + } + + @Test + void shouldLogoutWithEmptyConfig() { + assertEquals("login", underTest.logoutViewAction()); + } + + @Test + void shouldLogoutWithLogoutUrl() { + facadeMock.setClientLogoutUrl("https://client-logout-uri"); + oAuthConfiguration.setConfiguration(Oauth2ConfigurationImpl.builder().logoutUri("http://logout").build()); + assertNull(underTest.logoutViewAction()); + assertRedirect("https://client-logout-uri"); + } + + @Test + void logoutWithRedirectUri() { + facadeMock.setClientLogoutUrl("https://client-logout-uri"); + oAuthConfiguration.setConfiguration(Oauth2ConfigurationImpl.builder().logoutUri("http://logout") + .logoutRedirectParamName("post_logout_test_redirect_uri") + .postLogoutRedirectUri("https://post.logout.url").build()); + assertNull(underTest.logoutViewAction()); + assertRedirect("https://client-logout-uri?post_logout_test_redirect_uri=https%3A%2F%2Fpost.logout.url"); + } +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthRenewComponentTest.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthRenewComponentTest.java new file mode 100644 index 0000000..e463634 --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/OauthRenewComponentTest.java @@ -0,0 +1,42 @@ +package de.cuioss.portal.ui.oauth; + +import javax.enterprise.inject.Produces; +import javax.inject.Inject; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.junit.jupiter.api.BeforeEach; + +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.configuration.PortalConfigurationKeys; +import de.cuioss.portal.configuration.PortalConfigurationSource; +import de.cuioss.portal.core.test.mocks.configuration.PortalTestConfiguration; +import de.cuioss.portal.ui.runtime.application.view.HttpHeaderFilterImpl; +import de.cuioss.portal.ui.runtime.application.view.matcher.ViewMatcherProducer; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.test.jsf.component.AbstractComponentTest; + +@EnablePortalUiEnvironment +@AddBeanClasses({ Oauth2AuthenticationFacadeMock.class, WrappedOauthFacadeImpl.class, HttpHeaderFilterImpl.class, + ViewMatcherProducer.class, Oauth2ConfigurationProducerMock.class }) +class OauthRenewComponentTest extends AbstractComponentTest { + + @Produces + @LoginPagePath + private String loginUrl = "login.jsf"; + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacadeMock oauth2AuthenticationFacadeMock; + + @Inject + @PortalConfigurationSource + private PortalTestConfiguration configuration; + + @BeforeEach + void beforeTest() { + configuration.put(PortalConfigurationKeys.PORTAL_SESSION_TIMEOUT, "20"); + configuration.fireEvent(); + } + +} diff --git a/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/WrappedOauthFacadeImplTest.java b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/WrappedOauthFacadeImplTest.java new file mode 100644 index 0000000..91c49bc --- /dev/null +++ b/modules/portal-ui-oauth/src/test/java/de/cuioss/portal/ui/oauth/WrappedOauthFacadeImplTest.java @@ -0,0 +1,129 @@ +package de.cuioss.portal.ui.oauth; + +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.DESCRIPTOR_HOME; +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.DESCRIPTOR_PREFERENCES; +import static de.cuioss.portal.ui.test.configuration.PortalNavigationConfiguration.VIEW_PREFERENCES_LOGICAL_VIEW_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collections; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; + +import org.jboss.weld.junit5.auto.AddBeanClasses; +import org.jboss.weld.junit5.auto.EnableAlternatives; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.cuioss.jsf.api.application.navigation.ViewIdentifier; +import de.cuioss.jsf.api.common.view.ViewDescriptor; +import de.cuioss.jsf.api.servlet.ServletAdapterUtil; +import de.cuioss.portal.authentication.facade.PortalAuthenticationFacade; +import de.cuioss.portal.authentication.oauth.LoginPagePath; +import de.cuioss.portal.configuration.PortalConfigurationSource; +import de.cuioss.portal.core.test.mocks.configuration.PortalTestConfiguration; +import de.cuioss.portal.ui.api.history.PortalHistoryManager; +import de.cuioss.portal.ui.api.ui.context.CuiCurrentView; +import de.cuioss.portal.ui.runtime.application.view.HttpHeaderFilterImpl; +import de.cuioss.portal.ui.runtime.application.view.matcher.ViewMatcherProducer; +import de.cuioss.portal.ui.test.junit5.EnablePortalUiEnvironment; +import de.cuioss.portal.ui.test.mocks.PortalHistoryManagerMock; +import de.cuioss.test.jsf.util.JsfEnvironmentConsumer; +import de.cuioss.test.jsf.util.JsfEnvironmentHolder; +import de.cuioss.test.valueobjects.junit5.contracts.ShouldBeNotNull; +import de.cuioss.tools.net.ParameterFilter; +import lombok.Getter; +import lombok.Setter; + +@EnablePortalUiEnvironment +@AddBeanClasses({ Oauth2AuthenticationFacadeMock.class, HttpHeaderFilterImpl.class, ViewMatcherProducer.class, + Oauth2ConfigurationProducerMock.class }) +@EnableAlternatives(WrappedOauthFacadeImplTest.class) +class WrappedOauthFacadeImplTest implements ShouldBeNotNull, JsfEnvironmentConsumer { + + @Setter + @Getter + private JsfEnvironmentHolder environmentHolder; + + @Getter + @PortalWrappedOauthFacade + @Inject + private WrappedOauthFacadeImpl underTest; + + @Inject + @Getter + private OauthLoginPageBean loginPage; + + @Produces + @LoginPagePath + private String loginUrl = "login.jsf"; + + @Produces + HttpServletRequest servletRequest; + + @Inject + @PortalAuthenticationFacade + private Oauth2AuthenticationFacadeMock oauth2AuthenticationFacadeMock; + + @Inject + @PortalHistoryManager + private PortalHistoryManagerMock portalHistoryManagerMock; + + @Inject + @PortalConfigurationSource + private PortalTestConfiguration configuration; + + @BeforeEach + void beforeTest() { + servletRequest = ServletAdapterUtil.getRequest(getFacesContext()); + configuration.fireEvent(); + } + + @Test + void testRetrieveTokenShouldPass() { + oauth2AuthenticationFacadeMock.setTokenToRetrieve("token"); + assertEquals("token", underTest.retrieveToken("abc")); + } + + @Produces + @CuiCurrentView + @RequestScoped + @Alternative + ViewDescriptor getCurrentView() { + return DESCRIPTOR_PREFERENCES; + } + + @Test + void testRetrieveTokenShouldTriggerNew() { + oauth2AuthenticationFacadeMock.setTokenToRetrieve(null); + oauth2AuthenticationFacadeMock.setAuthenticated(true); + assertNull(underTest.retrieveToken("abc")); + assertNull(loginPage.testLoginViewAction()); + assertRedirect(VIEW_PREFERENCES_LOGICAL_VIEW_ID); + } + + @Test + void testRetrieveTargetView() { + // Create ViewIdentifier for Preferences page + var preferences = ViewIdentifier.getFromViewDesciptor(DESCRIPTOR_PREFERENCES, + new ParameterFilter(Collections.emptyList(), false)); + // Add to history manager + portalHistoryManagerMock.addCurrentUriToHistory(DESCRIPTOR_PREFERENCES); + // preserve the current (=preferences page) view + underTest.preserveCurrentView(); + // Add home to history manager + portalHistoryManagerMock.addCurrentUriToHistory(DESCRIPTOR_HOME); + // retrieveTargetView should return preferences page + assertEquals(preferences, underTest.retrieveTargetView()); + // Add home to history manager + portalHistoryManagerMock.addCurrentUriToHistory(DESCRIPTOR_HOME); + // retrieveTargetView should return home page at second call + assertEquals(ViewIdentifier.getFromViewDesciptor(DESCRIPTOR_HOME, + new ParameterFilter(Collections.emptyList(), false)), underTest.retrieveTargetView()); + } + +} diff --git a/modules/portal-ui-oauth/src/test/resources/META-INF/resources/javascript/SpecRunner.html b/modules/portal-ui-oauth/src/test/resources/META-INF/resources/javascript/SpecRunner.html new file mode 100644 index 0000000..30c0193 --- /dev/null +++ b/modules/portal-ui-oauth/src/test/resources/META-INF/resources/javascript/SpecRunner.html @@ -0,0 +1,36 @@ + + + + +Jasmine Spec Runner v2.0.1 + + + + + + + + + + + + + + + + + + +
      + + + +
      + + diff --git a/modules/portal-ui-oauth/src/test/resources/META-INF/resources/javascript/lib/jquery-1.11.1.min.js b/modules/portal-ui-oauth/src/test/resources/META-INF/resources/javascript/lib/jquery-1.11.1.min.js new file mode 100644 index 0000000..ab28a24 --- /dev/null +++ b/modules/portal-ui-oauth/src/test/resources/META-INF/resources/javascript/lib/jquery-1.11.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
      ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; +if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
      a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
      ","
      "],area:[1,"",""],param:[1,"",""],thead:[1,"","
      "],tr:[2,"","
      "],col:[2,"","
      "],td:[3,"","
      "],_default:k.htmlSerialize?[0,"",""]:[1,"X
      ","
      "]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("