chore: Initial import of FLEX training material

This commit is contained in:
Alexander Kobjolke 2024-11-07 21:02:53 +01:00
parent c01246d4f7
commit 12235acc42
1020 changed files with 53940 additions and 0 deletions

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.FilmSubscriber" />
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.FilmSubscriber" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.KinoSubscriber" />
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.KinoSubscriber" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.KontingentSubscriber" />
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.KontingentSubscriber" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.TicketSubscriber" />
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.TicketSubscriber" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.VorfuehrungSubscriber" />
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.api_in.event.VorfuehrungSubscriber" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.application.services.FilmService" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.application.services.KinoService" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.application.services.TicketBundle" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<Match>
<!-- spotbugs message is ... may expose internal representation by storing an externally mutable object into ... cache -->
<Class name="de.accso.flexinale.besucherportal.infrastructure.ActuatorEndpointCache" />
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
</FindBugsFilter>

View file

@ -0,0 +1,8 @@
FROM amazoncorretto:21.0.1-alpine3.18
WORKDIR /app
COPY ../../target/flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar /app
EXPOSE 8080
CMD ["java", "-jar", "flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar"]

View file

@ -0,0 +1 @@
docker build -t de.accso/flexinale-distributed-besucherportal:2024.3.0 -f Dockerfile ../../

View file

@ -0,0 +1,8 @@
FROM amazoncorretto:21.0.1-alpine3.18
WORKDIR /app
COPY ../../target/flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar /app
EXPOSE 8080
CMD ["java", "-jar", "flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar"]

View file

@ -0,0 +1 @@
podman build -t de.accso/flexinale-distributed-besucherportal:2024.3.0 -f Dockerfile ../../

View file

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed</artifactId>
<version>2024.3.0</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>flexinale-distributed-besucherportal</artifactId>
<version>2024.3.0</version>
<name>Flexinale Distributed Besucherportal</name>
<description>Flexinale - FLEX case-study &quot;film festival&quot;, distributed services, besucherportal</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-common</artifactId>
<version>2024.3.0</version>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-common</artifactId>
<version>2024.3.0</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-security</artifactId>
<scope>runtime</scope>
<version>2024.3.0</version>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-security_api_contract</artifactId>
<version>2024.3.0</version>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-besucherportal_api_contract</artifactId>
<version>2024.3.0</version>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-backoffice_api_contract</artifactId>
<version>2024.3.0</version>
</dependency>
<dependency>
<groupId>de.accso</groupId>
<artifactId>flexinale-distributed-ticketing_api_contract</artifactId>
<version>2024.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- name extension of spring boot fat jar-->
<configuration>
<classifier>spring-boot-fat-jar</classifier>
</configuration>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
<configuration>
<additionalProperties>
<flexinale.implementation>Distributed</flexinale.implementation>
<flexinale.app>Besucherportal</flexinale.app>
</additionalProperties>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${spotbugs-maven-plugin.version}</version>
<configuration>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>${findsecbugs-maven-plugin.version}</version>
</plugin>
</plugins>
<excludeFilterFile>SpotbugsExcludeFilter.xml</excludeFilterFile>
</configuration>
<dependencies>
<!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs</artifactId>
<version>${spotbugs.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,16 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed - start Besucherportal app (&quot;FlexinaleDistributedApplicationBesucherportal&quot;)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" folderName="App Distributed">
<option name="ENABLE_LAUNCH_OPTIMIZATION" value="false" />
<module name="flexinale-distributed-besucherportal" />
<option name="SPRING_BOOT_MAIN_CLASS" value="de.accso.flexinale.FlexinaleDistributedApplicationBesucherportal" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="de.accso.flexinale.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Cache Contents" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-cacheContents.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Events Consumed" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-consumed.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Events Published" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-published.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Health" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-health.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Info" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-info.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Metrics" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-metrics.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Distributed Besucherportal - Actuator Prometheus" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="Actuator Distributed" path="$PROJECT_DIR$/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-prometheus.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,14 @@
package de.accso.flexinale;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Profile;
@SpringBootApplication
@Profile({"!test-integrated &!test-distributed & !testdata"})
public class FlexinaleDistributedApplicationBesucherportal {
public static void main(String[] args) {
SpringApplication.run(FlexinaleDistributedApplicationBesucherportal.class, args);
}
}

View file

@ -0,0 +1,78 @@
package de.accso.flexinale.besucherportal.api_in.event;
import de.accso.flexinale.backoffice.api_contract.event.FilmCreatedEvent;
import de.accso.flexinale.backoffice.api_contract.event.FilmDeletedEvent;
import de.accso.flexinale.backoffice.api_contract.event.FilmUpdatedEvent;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import de.accso.flexinale.common.api.eventbus.EventSubscriber.EventSubscriber3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
@SuppressWarnings("unused")
public class FilmSubscriber implements EventSubscriber3<FilmCreatedEvent, FilmUpdatedEvent, FilmDeletedEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(FilmSubscriber.class);
private final FKKsVTInMemoryCache cache;
private final EventNotification notification;
@SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
public FilmSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final EventNotification notification) {
EventBus<FilmCreatedEvent> createBus = eventBusFactory.createOrGetEventBusFor(FilmCreatedEvent.class);
EventBus<FilmUpdatedEvent> updateBus = eventBusFactory.createOrGetEventBusFor(FilmUpdatedEvent.class);
EventBus<FilmDeletedEvent> deleteBus = eventBusFactory.createOrGetEventBusFor(FilmDeletedEvent.class);
createBus.subscribe(FilmCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
updateBus.subscribe(FilmUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
deleteBus.subscribe(FilmDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
this.cache = cache;
this.notification = notification;
}
@Override
public String getName() {
return FilmSubscriber.class.getName();
}
@Override
public String getGroupName() {
return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
}
@Override
public void receive(final FilmCreatedEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.film);
this.notification.notify(event);
}
@Override
public void receive2(final FilmUpdatedEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.film);
this.notification.notify(event);
}
@Override
public void receive3(final FilmDeletedEvent event) {
LOGGER.debug("received new event " + event);
cache.delete(event.film);
this.notification.notify(event);
}
public FKKsVTInMemoryCache getCache() {
return cache;
}
}

View file

@ -0,0 +1,101 @@
package de.accso.flexinale.besucherportal.api_in.event;
import de.accso.flexinale.backoffice.api_contract.event.KinoCreatedEvent;
import de.accso.flexinale.backoffice.api_contract.event.KinoDeletedEvent;
import de.accso.flexinale.backoffice.api_contract.event.KinoUpdatedEvent;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import de.accso.flexinale.common.api.eventbus.EventSubscriber.EventSubscriber3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
@SuppressWarnings("unused")
public class KinoSubscriber implements EventSubscriber3<KinoCreatedEvent, KinoUpdatedEvent, KinoDeletedEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(KinoSubscriber.class);
private final FKKsVTInMemoryCache cache;
private final EventNotification notification;
@SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
public KinoSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final EventNotification notification) {
EventBus<KinoCreatedEvent> createBus = eventBusFactory.createOrGetEventBusFor(KinoCreatedEvent.class);
EventBus<KinoUpdatedEvent> updateBus = eventBusFactory.createOrGetEventBusFor(KinoUpdatedEvent.class);
EventBus<KinoDeletedEvent> deleteBus = eventBusFactory.createOrGetEventBusFor(KinoDeletedEvent.class);
createBus.subscribe(KinoCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
updateBus.subscribe(KinoUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
deleteBus.subscribe(KinoDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
this.cache = cache;
this.notification = notification;
}
@Override
public String getName() {
return KinoSubscriber.class.getName();
}
@Override
public String getGroupName() {
return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
}
@Override
public void receive(final KinoCreatedEvent event) {
LOGGER.debug("received new event " + event);
KinoTO fixedKino = fixKinoSaalTO2KinoTORelation(event.kino);
cache.addOrUpdate(fixedKino);
this.notification.notify(event);
}
@Override
public void receive2(final KinoUpdatedEvent event) {
LOGGER.debug("received new event " + event);
KinoTO fixedKino = fixKinoSaalTO2KinoTORelation(event.kino);
cache.addOrUpdate(fixedKino);
this.notification.notify(event);
}
@Override
public void receive3(final KinoDeletedEvent event) {
LOGGER.debug("received new event " + event);
cache.delete(event.kino);
this.notification.notify(event);
}
// need to fix in all KinoSaalTO the relation to the correct KinoTO (as it is null after Json deserialization)
private KinoTO fixKinoSaalTO2KinoTORelation(final KinoTO kinoTOFromEvent) {
if (kinoTOFromEvent == null) return null;
final KinoTO newKinoTO = new KinoTO(kinoTOFromEvent.id(), kinoTOFromEvent.version(), kinoTOFromEvent.name(),
kinoTOFromEvent.adresse(), kinoTOFromEvent.emailAdresse(), new HashSet<>());
kinoTOFromEvent.kinoSaele()
.stream()
.map(kinoSaalTOFromEvent ->
new KinoSaalTO(kinoSaalTOFromEvent.id(), kinoSaalTOFromEvent.version(),
kinoSaalTOFromEvent.name(), kinoSaalTOFromEvent.anzahlPlaetze(), newKinoTO))
.forEach(kinoSaalTOFromEvent ->
newKinoTO.kinoSaele().add(kinoSaalTOFromEvent));
return newKinoTO;
}
public FKKsVTInMemoryCache getCache() {
return cache;
}
}

View file

@ -0,0 +1,54 @@
package de.accso.flexinale.besucherportal.api_in.event;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import de.accso.flexinale.ticketing.api_contract.event.OnlineKontingentChangedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
@SuppressWarnings("unused")
public class KontingentSubscriber implements EventSubscriber<OnlineKontingentChangedEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(KontingentSubscriber.class);
private final FKKsVTInMemoryCache cache;
private final EventNotification notification;
public KontingentSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final EventNotification notification) {
EventBus<OnlineKontingentChangedEvent> kontingentBus = eventBusFactory.createOrGetEventBusFor(OnlineKontingentChangedEvent.class);
kontingentBus.subscribe(OnlineKontingentChangedEvent.class, this, START_READING_FROM_BEGINNING);
this.cache = cache;
this.notification = notification;
}
@Override
public String getName() {
return KontingentSubscriber.class.getName();
}
@Override
public String getGroupName() {
return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
}
@Override
public void receive(final OnlineKontingentChangedEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.onlineKontingent.vorfuehrungId(), getRawOrNull(event.onlineKontingent.neuesOnlineRestKontingent()));
this.notification.notify(event);
}
public FKKsVTInMemoryCache getCache() {
return cache;
}
}

View file

@ -0,0 +1,66 @@
package de.accso.flexinale.besucherportal.api_in.event;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import de.accso.flexinale.ticketing.api_contract.event.TicketGekauftEvent;
import de.accso.flexinale.ticketing.api_contract.event.TicketUngueltigEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
@SuppressWarnings("unused")
public class TicketSubscriber implements EventSubscriber.EventSubscriber2<TicketGekauftEvent, TicketUngueltigEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(TicketSubscriber.class);
private final FKKsVTInMemoryCache cache;
private final EventNotification notification;
@SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
public TicketSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final EventNotification notification) {
EventBus<TicketGekauftEvent> createBus = eventBusFactory.createOrGetEventBusFor(TicketGekauftEvent.class);
EventBus<TicketUngueltigEvent> ungueltigBus = eventBusFactory.createOrGetEventBusFor(TicketUngueltigEvent.class);
createBus.subscribe(TicketGekauftEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
ungueltigBus.subscribe(TicketUngueltigEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
this.cache = cache;
this.notification = notification;
}
@Override
public String getName() {
return TicketSubscriber.class.getName();
}
@Override
public String getGroupName() {
return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
}
@Override
public void receive(final TicketGekauftEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.ticket);
this.notification.notify(event);
}
@Override
public void receive2(final TicketUngueltigEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.ticket);
this.notification.notify(event);
}
public FKKsVTInMemoryCache getCache() {
return cache;
}
}

View file

@ -0,0 +1,77 @@
package de.accso.flexinale.besucherportal.api_in.event;
import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCreatedEvent;
import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungDeletedEvent;
import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungUpdatedEvent;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
@SuppressWarnings("unused")
public class VorfuehrungSubscriber implements EventSubscriber.EventSubscriber3<VorfuehrungCreatedEvent, VorfuehrungUpdatedEvent, VorfuehrungDeletedEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungSubscriber.class);
private final FKKsVTInMemoryCache cache;
private final EventNotification notification;
@SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
public VorfuehrungSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final EventNotification notification) {
EventBus<VorfuehrungCreatedEvent> createBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class);
EventBus<VorfuehrungUpdatedEvent> updateBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class);
EventBus<VorfuehrungDeletedEvent> deleteBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungDeletedEvent.class);
createBus.subscribe(VorfuehrungCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
updateBus.subscribe(VorfuehrungUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
deleteBus.subscribe(VorfuehrungDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
this.cache = cache;
this.notification = notification;
}
@Override
public String getName() {
return VorfuehrungSubscriber.class.getName();
}
@Override
public String getGroupName() {
return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
}
@Override
public void receive(final VorfuehrungCreatedEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.vorfuehrung);
this.notification.notify(event);
}
@Override
public void receive2(final VorfuehrungUpdatedEvent event) {
LOGGER.debug("received new event " + event);
cache.addOrUpdate(event.vorfuehrung);
this.notification.notify(event);
}
@Override
public void receive3(final VorfuehrungDeletedEvent event) {
LOGGER.debug("received new event " + event);
cache.delete(event.vorfuehrung);
this.notification.notify(event);
}
public FKKsVTInMemoryCache getCache() {
return cache;
}
}

View file

@ -0,0 +1,110 @@
package de.accso.flexinale.besucherportal.api_in.web;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.besucherportal.application.services.FilmService;
import de.accso.flexinale.besucherportal.application.services.KinoService;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.common.shared_kernel.RawWrapper;
import de.accso.flexinale.security.api_contract.BesucherRetriever;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
@Controller
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
public class FilmWebController {
private record VorfuehrungMitRestkontingentTO(Identifiable.Id vorfuehrungId, FilmTO film, Zeit zeit,
KinoSaalTO kinoSaal, KinoTO kino, RestKontingent restkontingentOnline) {
public record Zeit(LocalDateTime raw) implements RawWrapper<LocalDateTime> {
public Zeit(LocalDateTime raw) {
this.raw = raw.withNano(0); // precision is second
}
}
public record RestKontingent(Integer raw) implements RawWrapper<Integer> {}
}
@Autowired
private FilmService filmService;
@Autowired
private KinoService kinoService;
@Autowired
private BesucherRetriever besucherRetriever;
@Autowired
private FKKsVTInMemoryCache cache;
@GetMapping(value="/filme")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
@SuppressWarnings("SameReturnValue")
public String filme(final Model model) {
List<FilmTO> filme = filmService.filme();
model.addAttribute("filme", filme);
return "filme";
}
@GetMapping(value="/film/{id}")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
@SuppressWarnings("SameReturnValue")
public String film(@PathVariable("id") final String id, final Model model) {
Identifiable.Id filmId = Identifiable.Id.of(id);
FilmTO film = filmService.film(filmId);
if (film == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "film %s not available".formatted(id));
}
else {
model.addAttribute("film", film);
List<VorfuehrungTO> vorfuehrungenOfFilm = filmService.vorfuehrungenFuer(filmId);
List<VorfuehrungMitRestkontingentTO> vorfuehrungenMitRestkontingent = vorfuehrungenOfFilm
.stream()
.map(v -> getRestkontingentOnlineAndMap(v, film))
.collect(Collectors.toList());
model.addAttribute("vorfuehrungenMitRestkontingent", vorfuehrungenMitRestkontingent);
if (!vorfuehrungenOfFilm.isEmpty()) {
Identifiable.Id idOfLoggedInBesucher = besucherRetriever.getIdOfLoggedInBesucher();
List<String> vorfuehrungenMitUeberlapp =
filmService.vorfuehrungenMitUeberlapp(vorfuehrungenOfFilm, idOfLoggedInBesucher);
model.addAttribute("vorfuehrungenMitUeberlapp", vorfuehrungenMitUeberlapp);
List<String> vorfuehrungenMitTicket =
filmService.vorfuehrungenFuerDieDerBenutzerEinTicketHat(vorfuehrungenOfFilm, idOfLoggedInBesucher);
model.addAttribute("vorfuehrungenMitTicket", vorfuehrungenMitTicket);
}
}
return "film";
}
private VorfuehrungMitRestkontingentTO getRestkontingentOnlineAndMap(final VorfuehrungTO vorfuehrung,
final FilmTO film) {
VorfuehrungMitRestkontingentTO.RestKontingent restKontingentOnline =
new VorfuehrungMitRestkontingentTO.RestKontingent(cache.getRestkontingentOnline(vorfuehrung.id()));
KinoTO kino = kinoService.kino(vorfuehrung.kinoId());
return new VorfuehrungMitRestkontingentTO(vorfuehrung.id(), film,
new VorfuehrungMitRestkontingentTO.Zeit(getRawOrNull(vorfuehrung.zeit())),
vorfuehrung.kinoSaal(), kino, restKontingentOnline);
}
}

View file

@ -0,0 +1,25 @@
package de.accso.flexinale.besucherportal.api_in.web;
import de.accso.flexinale.common.application.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
public class IndexWebController {
@Autowired
private Config config;
@GetMapping(value="/")
@SuppressWarnings("SameReturnValue")
public String index(final Model model) {
model.addAttribute("applicationTitle", config.getApplicationTitle());
model.addAttribute("buildVersion", config.getBuildVersion());
model.addAttribute("buildDate", config.getBuildDate());
return "index";
}
}

View file

@ -0,0 +1,48 @@
package de.accso.flexinale.besucherportal.api_in.web;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.besucherportal.application.services.KinoService;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Controller
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
public class KinoWebController {
@Autowired
private KinoService kinoService;
@GetMapping(value="/kinos")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
@SuppressWarnings("SameReturnValue")
public String kinos(final Model model) {
List<KinoTO> kinos = kinoService.kinos();
model.addAttribute("kinos", kinos);
return "kinos";
}
@GetMapping(value="/kino/{id}")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
@SuppressWarnings("SameReturnValue")
public String kino(@PathVariable("id") final String id, final Model model) {
Identifiable.Id kinoId = Identifiable.Id.of(id);
KinoTO kino = kinoService.kino(kinoId);
if (kino == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "kino %s not available".formatted(id));
}
else {
model.addAttribute("kino", kino);
return "kino";
}
}
}

View file

@ -0,0 +1,166 @@
package de.accso.flexinale.besucherportal.api_in.web;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
class TicketSorter {
private static final Logger LOGGER = LoggerFactory.getLogger(TicketSorter.class);
/**
* return a list of list of tickets, sorted by time (inner list) and date (outer list).
* each list contains the tickets for a day
*/
public static List<List<VorfuehrungMitAnzahlTicketsTO>> sortAndMapTicketsPerDay(final List<TicketTO> allTickets,
final FKKsVTInMemoryCache cache) {
if (allTickets == null || allTickets.isEmpty()) {
return new ArrayList<>();
}
Map<TicketTO, VorfuehrungTO> ticket2Vorfuehrung = createTicketToVorfuehrungMap(allTickets, cache);
// 0) sort tickets by time (not done in database SQL query as before)
allTickets.sort(new TicketTOByZeitSortingComparator(ticket2Vorfuehrung));
// 1) tickets by day
List<List<TicketTO>> ticketsProTag = new ArrayList<>();
LocalDate previousDate = LocalDate.MIN; // far past :-)
for (TicketTO ticket : allTickets) {
LocalDateTime zeit = getRawOrNull(ticket2Vorfuehrung.get(ticket).zeit());
LocalDate currentDate = zeit.toLocalDate();
if (currentDate.equals(previousDate)) {
ticketsProTag.getLast().add(ticket);
}
else {
List<TicketTO> tickets = new ArrayList<>();
tickets.add(ticket);
ticketsProTag.add(tickets);
}
previousDate = currentDate;
}
// 2) inner structure: Vorfuehrung and tickets
List<List<VorfuehrungMitAnzahlTicketsTO>> vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag = new ArrayList<>();
for (List<TicketTO> ticketsForOneDay : ticketsProTag) {
List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenUndAnzahlTicketsFuerEinenTag = new ArrayList<>();
int anzahlGueltigeTicketsFuerVorfuehrung = 0;
int anzahlUngueltigeTicketsFuerVorfuehrung = 0;
VorfuehrungTO previousVorfuehrung = ticket2Vorfuehrung.get(ticketsForOneDay.getFirst());
for (TicketTO ticket : ticketsForOneDay) {
// inside each day, Vorfuehrungen are ordered by zeit.
// There is never more than one Vorfuehrung at a zeit for a user.
// So tickets for the same Vorfuehrung are all in a row - that's why the following works.
VorfuehrungTO currentVorfuehrung = ticket2Vorfuehrung.get(ticket);
if (currentVorfuehrung.equals(previousVorfuehrung)) {
Boolean ticketGueltig = getRawOrNull(ticket.gueltig());
if (ticketGueltig) {
anzahlGueltigeTicketsFuerVorfuehrung++;
}
else {
anzahlUngueltigeTicketsFuerVorfuehrung++;
}
}
else {
Boolean ticketGueltig = getRawOrNull(ticket.gueltig());
// add the information for previous Vorfuehrung
VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung,
previousVorfuehrung, cache);
vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
// ... we also have a new ticket for the next Vorfuehrung
if (ticketGueltig) {
anzahlGueltigeTicketsFuerVorfuehrung = 1;
anzahlUngueltigeTicketsFuerVorfuehrung = 0;
} else {
anzahlGueltigeTicketsFuerVorfuehrung = 0;
anzahlUngueltigeTicketsFuerVorfuehrung = 1;
}
}
previousVorfuehrung = currentVorfuehrung;
}
// Also add the last Vorfuehrung an anzahlTickets when day is over
VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung, previousVorfuehrung, cache);
vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag.add(vorfuehrungenUndAnzahlTicketsFuerEinenTag);
}
return vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag;
}
private static VorfuehrungMitAnzahlTicketsTO mapVorfuehrungToTO(final int anzahlGueltigeTicketsFuerVorfuehrung,
final int anzahlUngueltigeTicketsFuerVorfuehrung,
final VorfuehrungTO vorfuehrung,
final FKKsVTInMemoryCache cache) {
FilmTO film = vorfuehrung.film();
KinoTO kino = cache.kino(vorfuehrung.kinoId());
if (film == null) {
throw new FlexinaleIllegalStateException("no Film for Vorfuehrung %s found".formatted(vorfuehrung.id()));
}
else {
return new VorfuehrungMitAnzahlTicketsTO(
new VorfuehrungMitAnzahlTicketsTO.Zeit(getRawOrNull(vorfuehrung.zeit())),
film,
vorfuehrung.kinoSaal(),
kino,
new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlGueltigeTicketsFuerVorfuehrung),
new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlUngueltigeTicketsFuerVorfuehrung)
);
}
}
private static Map<TicketTO, VorfuehrungTO> createTicketToVorfuehrungMap(final List<TicketTO> allTickets,
final FKKsVTInMemoryCache cache) {
Map<TicketTO, VorfuehrungTO> ticketToVorfuehrung = new HashMap<>();
for (TicketTO ticket : allTickets) {
VorfuehrungTO vorfuehrung = cache.vorfuehrung(ticket.vorfuehrungId());
if (vorfuehrung == null) {
LOGGER.error("Vorfuehrung %s of ticket %s cannot be found.".formatted(ticket.vorfuehrungId(), ticket.id()));
}
else {
ticketToVorfuehrung.put(ticket, vorfuehrung);
}
}
return ticketToVorfuehrung;
}
}
class TicketTOByZeitSortingComparator implements Comparator<TicketTO>, Serializable {
private final Map<TicketTO, VorfuehrungTO> ticket2Vorfuehrung;
TicketTOByZeitSortingComparator(final Map<TicketTO, VorfuehrungTO> ticket2Vorfuehrung) {
this.ticket2Vorfuehrung = ticket2Vorfuehrung;
}
@Override
public int compare(final TicketTO t1, final TicketTO t2) {
VorfuehrungTO v1 = ticket2Vorfuehrung.get(t1);
VorfuehrungTO v2 = ticket2Vorfuehrung.get(t2);
LocalDateTime v1Zeit = getRawOrNull(v1.zeit());
LocalDateTime v2Zeit = getRawOrNull(v2.zeit());
return v1Zeit.compareTo(v2Zeit);
}
}

View file

@ -0,0 +1,89 @@
package de.accso.flexinale.besucherportal.api_in.web;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import de.accso.flexinale.besucherportal.application.services.TicketService;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.common.shared_kernel.RawWrapper;
import de.accso.flexinale.security.api_contract.BesucherRetriever;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.time.LocalDateTime;
import java.util.List;
@Controller
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
public class TicketWebController {
private static final Logger LOGGER = LoggerFactory.getLogger(TicketWebController.class);
@Autowired
private TicketService ticketService;
@Autowired
private FKKsVTInMemoryCache cache;
@Autowired
private BesucherRetriever besucherRetriever;
@GetMapping(value="/tickets")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public String listTickets(final Model model) {
Identifiable.Id besucherId = besucherRetriever.getIdOfLoggedInBesucher();
List<TicketTO> allTickets = cache.ticketsByBesucher(besucherId);
List<List<VorfuehrungMitAnzahlTicketsTO>> ticketsByDay
= TicketSorter.sortAndMapTicketsPerDay(allTickets, cache);
model.addAttribute("ticketsByDay", ticketsByDay);
int totalNumberOfTickets = cache.gesamtZahlDerTicketsFuer(besucherId);
model.addAttribute("totalNumberOfTickets", totalNumberOfTickets);
return ("tickets");
}
@PostMapping("/vorfuehrung/loeseGutscheineOnlineEin")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public String loeseGutscheineOnlineEin(
@RequestParam("vorfuehrungId") final String vId,
@RequestParam("filmId") final String fId,
@RequestParam("anzahl") final int anzahl,
RedirectAttributes redirAttrs) {
Identifiable.Id vorfuehrungId = Identifiable.Id.of(vId);
Identifiable.Id filmId = Identifiable.Id.of(fId);
Identifiable.Id besucherId = besucherRetriever.getIdOfLoggedInBesucher();
ticketService.loeseGutscheineOnlineFuerVorfuehrungEin(vorfuehrungId, filmId, besucherId, anzahl);
redirAttrs.addFlashAttribute("success",
"Kauf von %d Ticket(s) beauftragt".formatted(anzahl));
return "redirect:/tickets";
}
}
// class used to "flatten" Vorfuehrung and number of Tickets for Vorfuehrung for Clients
record VorfuehrungMitAnzahlTicketsTO(Zeit zeit, FilmTO film,
KinoSaalTO kinoSaal, KinoTO kino,
AnzahlTickets anzahlGueltigeTickets, AnzahlTickets anzahlUngueltigeTickets) {
public record Zeit(LocalDateTime raw) implements RawWrapper<LocalDateTime> {
public Zeit(LocalDateTime raw) {
this.raw = raw.withNano(0); // precision is second
}
}
public record AnzahlTickets(Integer raw) implements RawWrapper<Integer> {}
}

View file

@ -0,0 +1,46 @@
package de.accso.flexinale.besucherportal.api_out.event;
import de.accso.flexinale.besucherportal.api_contract.event.GutscheinEinloesenBeauftragtEvent;
import de.accso.flexinale.besucherportal.api_contract.event.model.GutscheinEinloesenAuftragTO;
import de.accso.flexinale.besucherportal.application.services.GutscheinEinloesenBeauftragtPublication;
import de.accso.flexinale.common.api.eventbus.EventBus;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.eventbus.EventPublisher;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.common.shared_kernel.Versionable;
public class GutscheinEinloesenBeauftragtPublisher implements EventPublisher<GutscheinEinloesenBeauftragtEvent>, GutscheinEinloesenBeauftragtPublication {
private final EventBus<GutscheinEinloesenBeauftragtEvent> gutscheinEinloesenBeauftragtEventBus;
private final EventNotification notification;
public GutscheinEinloesenBeauftragtPublisher(final EventBusFactory eventBusFactory,
final EventNotification notification) {
this.gutscheinEinloesenBeauftragtEventBus = eventBusFactory.createOrGetEventBusFor(GutscheinEinloesenBeauftragtEvent.class);
this.notification = notification;
}
@Override
public void publishGutscheinEinloesenBeauftragtEvent(final Identifiable.Id vorfuehrungId,
final Identifiable.Id filmId,
final Identifiable.Id besucherId,
final int anzahlGutscheine) {
GutscheinEinloesenAuftragTO gutschein = new GutscheinEinloesenAuftragTO(Identifiable.Id.of(), Versionable.initialVersion(),
filmId, vorfuehrungId, besucherId,
new GutscheinEinloesenAuftragTO.AnzahlTickets(anzahlGutscheine));
post(GutscheinEinloesenBeauftragtEvent.class,
new GutscheinEinloesenBeauftragtEvent(gutschein));
}
@Override
public String getName() {
return GutscheinEinloesenBeauftragtPublisher.class.getName();
}
@Override
public void post(Class<GutscheinEinloesenBeauftragtEvent> eventType, GutscheinEinloesenBeauftragtEvent event) {
this.gutscheinEinloesenBeauftragtEventBus.publish(eventType, event);
this.notification.notify(event);
}
}

View file

@ -0,0 +1,127 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.common.application.caching.InMemoryCache;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import java.util.List;
import java.util.stream.Collectors;
import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
public final class FKKsVTInMemoryCache {
// caches are currently never cleared - might want to use Cache implementation with automatic time-to-live
private final InMemoryCache<Identifiable.Id, FilmTO> filmCache = new InMemoryCache<>();
private final InMemoryCache<Identifiable.Id, KinoTO> kinoCache = new InMemoryCache<>();
private final InMemoryCache<Identifiable.Id, VorfuehrungTO> vorfuehrungCache = new InMemoryCache<>();
private final InMemoryCache<Identifiable.Id, TicketTO> ticketCache = new InMemoryCache<>();
private final InMemoryCache<Identifiable.Id, Integer> vorfuehrung2RestKontingentOnlineCache = new InMemoryCache<>();
public void addOrUpdate(final FilmTO film) {
filmCache.put(film.id(), film);
}
public void delete(final FilmTO film) {
filmCache.remove(film.id());
}
public FilmTO film(final Identifiable.Id id) {
return filmCache.get(id);
}
public List<FilmTO> filme() {
return filmCache.values().stream().toList();
}
// -------------------------------------------------------------------------------------------------------------
public void addOrUpdate(final KinoTO kino) {
kinoCache.put(kino.id(), kino);
}
public void delete(final KinoTO kino) {
kinoCache.remove(kino.id());
}
public KinoTO kino(final Identifiable.Id id) {
return kinoCache.get(id);
}
public List<KinoTO> kinos() {
return kinoCache.values().stream().toList();
}
// -------------------------------------------------------------------------------------------------------------
public void addOrUpdate(final VorfuehrungTO vorfuehrung) {
vorfuehrungCache.put(vorfuehrung.id(), vorfuehrung);
}
public void delete(final VorfuehrungTO vorfuehrung) {
vorfuehrungCache.remove(vorfuehrung.id());
}
public List<VorfuehrungTO> vorfuehrungen() {
return vorfuehrungCache.values().stream().toList();
}
public VorfuehrungTO vorfuehrung(final Identifiable.Id id) {
return vorfuehrungCache.get(id);
}
public List<VorfuehrungTO> vorfuehrungenByFilmId(final Identifiable.Id filmId) {
return vorfuehrungCache.values()
.stream()
.filter(v -> v.film().id().equals(filmId))
.toList();
}
// -------------------------------------------------------------------------------------------------------------
public void addOrUpdate(final TicketTO ticket) {
ticketCache.put(ticket.id(), ticket);
}
public List<TicketTO> tickets() {
return ticketCache.values().stream().toList();
}
public List<TicketTO> ticketsByBesucher(final Identifiable.Id besucherId) {
return ticketCache.values()
.stream()
.filter(t -> t.besucherId().equals(besucherId))
.collect(Collectors.toList()); // don't use .toList() here as we need to sort the list later
}
public List<TicketTO> gueltigeTicketsByBesucher(final Identifiable.Id besucherId) {
return ticketsByBesucher(besucherId)
.stream()
.filter((t -> getRawOrNull(t.gueltig())))
.collect(Collectors.toList());
}
public int gesamtZahlDerTicketsFuer(final Identifiable.Id besucherId) {
return (int) ticketCache.values()
.stream()
.filter(t -> t.besucherId().equals(besucherId))
.filter((t -> getRawOrNull(t.gueltig())))
.count();
}
// -------------------------------------------------------------------------------------------------------------
public void addOrUpdate(final Identifiable.Id vorfuehrungId, final Integer onlineRestKontingent) {
vorfuehrung2RestKontingentOnlineCache.put(vorfuehrungId, onlineRestKontingent);
}
public Integer getRestkontingentOnline(final Identifiable.Id vorfuehrungId) {
Integer restKontingentFromCache = vorfuehrung2RestKontingentOnlineCache.get(vorfuehrungId);
return (restKontingentFromCache != null) ? restKontingentFromCache : 0;
}
}

View file

@ -0,0 +1,65 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import java.util.ArrayList;
import java.util.List;
public class FilmService {
private final FKKsVTInMemoryCache cache;
private final VorfuehrungService vorfuehrungService;
private final long minZeitZwischenVorfuehrungenInMinuten;
public FilmService(final FKKsVTInMemoryCache cache,
final VorfuehrungService vorfuehrungService,
final long minZeitZwischenVorfuehrungenInMinuten) {
this.cache = cache;
this.vorfuehrungService = vorfuehrungService;
this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten;
}
public List<FilmTO> filme() {
return cache.filme();
}
public FilmTO film(final Identifiable.Id id) {
return cache.film(id);
}
public List<VorfuehrungTO> vorfuehrungenFuer(final Identifiable.Id filmId) {
return cache.vorfuehrungenByFilmId(filmId);
}
public List<String> vorfuehrungenMitUeberlapp(final List<VorfuehrungTO> vorfuehrungen,
final Identifiable.Id besucherId) {
List<String> vorfuehrungenMitUeberlapp = new ArrayList<>();
TicketBundle ticketBundle = new TicketBundle(besucherId, cache, vorfuehrungService, minZeitZwischenVorfuehrungenInMinuten);
for (VorfuehrungTO vorfuehrung : vorfuehrungen) {
if (ticketBundle.mindestensEinGueltigesTicketImTicketBundleUeberlapptMit(vorfuehrung)) {
vorfuehrungenMitUeberlapp.add(vorfuehrung.id().id());
}
}
return vorfuehrungenMitUeberlapp;
}
public List<String> vorfuehrungenFuerDieDerBenutzerEinTicketHat(final List<VorfuehrungTO> vorfuehrungen,
final Identifiable.Id besucherId) {
List<String> vorfuehrungenMitTicket = new ArrayList<>();
TicketBundle ticketBundle = new TicketBundle(besucherId, cache, vorfuehrungService, minZeitZwischenVorfuehrungenInMinuten);
for (VorfuehrungTO vorfuehrung : vorfuehrungen) {
if (ticketBundle.hatSchonGueltigesTicketFuer(vorfuehrung)) {
vorfuehrungenMitTicket.add(vorfuehrung.id().id());
}
}
return vorfuehrungenMitTicket;
}
}

View file

@ -0,0 +1,10 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.common.shared_kernel.Identifiable;
public interface GutscheinEinloesenBeauftragtPublication {
void publishGutscheinEinloesenBeauftragtEvent(final Identifiable.Id vorfuehrungId,
final Identifiable.Id filmId,
final Identifiable.Id besucherId,
final int anzahlGutscheine);
}

View file

@ -0,0 +1,23 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import java.util.List;
public class KinoService {
private final FKKsVTInMemoryCache cache;
public KinoService(final FKKsVTInMemoryCache cache) {
this.cache = cache;
}
public List<KinoTO> kinos() {
return cache.kinos();
}
public KinoTO kino(final Identifiable.Id id) {
return cache.kino(id);
}
}

View file

@ -0,0 +1,65 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
public class TicketBundle {
private static final Logger LOGGER = LoggerFactory.getLogger(TicketBundle.class);
private final FKKsVTInMemoryCache cache;
private final VorfuehrungService vorfuehrungService;
private final long minZeitZwischenVorfuehrungenInMinuten;
private final List<TicketTO> gueltigeTicketsOfBesucher;
public TicketBundle(final Identifiable.Id besucherId, final FKKsVTInMemoryCache cache,
final VorfuehrungService vorfuehrungService,
final long minZeitZwischenVorfuehrungenInMinuten) {
this.cache = cache;
this.vorfuehrungService = vorfuehrungService;
this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten;
this.gueltigeTicketsOfBesucher = cache.gueltigeTicketsByBesucher(besucherId);
}
public boolean mindestensEinGueltigesTicketImTicketBundleUeberlapptMit(final VorfuehrungTO vorfuehrung) {
FilmTO filmOfVorfuehrung = vorfuehrung.film();
return gueltigeTicketsOfBesucher.stream().anyMatch(ticket -> {
VorfuehrungTO vorfuehrungOfTicket = cache.vorfuehrung(ticket.vorfuehrungId());
if (vorfuehrungOfTicket == null)
return false;
else {
FilmTO filmOfVorfuehrungOfTicket = vorfuehrungOfTicket.film();
if (filmOfVorfuehrungOfTicket == null) {
String message = "Film for Vorfuehrung %s could not be found".formatted(vorfuehrungOfTicket.id());
LOGGER.error(message);
throw new FlexinaleIllegalStateException(message);
}
return vorfuehrungService.vorfuehrungUeberlapptMit(vorfuehrung, vorfuehrungOfTicket,
getRawOrNull(filmOfVorfuehrung.dauerInMinuten()),
getRawOrNull(filmOfVorfuehrungOfTicket.dauerInMinuten()),
minZeitZwischenVorfuehrungenInMinuten);
}
});
}
public boolean hatSchonGueltigesTicketFuer(final VorfuehrungTO vorfuehrung) {
return gueltigeTicketsOfBesucher.stream()
.map(TicketTO::vorfuehrungId)
.toList()
.contains(vorfuehrung.id());
}
}

View file

@ -0,0 +1,20 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.common.shared_kernel.Identifiable;
public class TicketService {
final GutscheinEinloesenBeauftragtPublication gutscheinEinloesenBeauftragtPublisher;
public TicketService(final GutscheinEinloesenBeauftragtPublication gutscheinEinloesenBeauftragtPublisher) {
this.gutscheinEinloesenBeauftragtPublisher = gutscheinEinloesenBeauftragtPublisher;
}
public void loeseGutscheineOnlineFuerVorfuehrungEin(final Identifiable.Id vorfuehrungId,
final Identifiable.Id filmId,
final Identifiable.Id besucherId,
final int anzahlGutscheine) {
gutscheinEinloesenBeauftragtPublisher
.publishGutscheinEinloesenBeauftragtEvent(vorfuehrungId, filmId, besucherId, anzahlGutscheine);
}
}

View file

@ -0,0 +1,22 @@
package de.accso.flexinale.besucherportal.application.services;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import java.time.LocalDateTime;
import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
public final class VorfuehrungService {
public boolean vorfuehrungUeberlapptMit(final VorfuehrungTO one, final VorfuehrungTO other,
final Integer oneDauerInMinuten, final Integer otherDauerInMinuten,
final long puffer) {
LocalDateTime begin = getRawOrNull(one.zeit());
LocalDateTime beginOther = getRawOrNull(other.zeit());
LocalDateTime end = begin.plusMinutes(oneDauerInMinuten);
LocalDateTime endOther = beginOther.plusMinutes(otherDauerInMinuten);
return ((begin.isBefore(endOther.plusMinutes(puffer)))
&& (end.isAfter(beginOther.minusMinutes(puffer))));
}
}

View file

@ -0,0 +1,43 @@
package de.accso.flexinale.besucherportal.infrastructure;
import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
@Endpoint(id = "besucherportalCacheContents")
public class ActuatorEndpointCache {
private final FKKsVTInMemoryCache cache;
public ActuatorEndpointCache(final FKKsVTInMemoryCache cache) {
this.cache = cache;
}
@ReadOperation
public List<?> cacheContents() {
return List.of(
cache.filme(),
cache.kinos(),
cache.vorfuehrungen(),
cache.tickets()
);
}
@ReadOperation
public List<?> cacheContentsFilteredBy(@Selector String contentType) {
return switch (contentType) {
case "filme" -> cache.filme();
case "kinos" -> cache.kinos();
case "vorfuehrungen" -> cache.vorfuehrungen();
case "tickets" -> cache.tickets();
default -> throw new UnsupportedOperationException("type %s not supported".formatted(contentType));
};
}
}

View file

@ -0,0 +1,36 @@
package de.accso.flexinale.besucherportal.infrastructure;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.event.Event;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@Component
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
@Endpoint(id = "besucherportalEventsConsumed")
public class FlexinaleBesucherPortalActuatorEndpointEventsConsumed implements EventNotification {
private final Queue<Event> eventsConsumed = new ConcurrentLinkedQueue<>(); // event list is ordered by consumption time
@ReadOperation
public List<Event> eventsConsumed() {
return eventsConsumed.stream().toList();
}
@ReadOperation
public List<Event> eventsConsumedFilteredBy(@Selector Identifiable.Id correlationId) {
return eventsConsumed.stream().filter(event -> event.correlationId().equals(correlationId)).toList();
}
@Override
public void notify(final Event event) {// might want to use a generic subscriber
eventsConsumed.add(event);
}
}

View file

@ -0,0 +1,36 @@
package de.accso.flexinale.besucherportal.infrastructure;
import de.accso.flexinale.common.api.eventbus.EventNotification;
import de.accso.flexinale.common.api.event.Event;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@Component
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
@Endpoint(id = "besucherportalEventsPublished")
public class FlexinaleBesucherPortalActuatorEndpointEventsPublished implements EventNotification {
private final Queue<Event> eventsPublished = new ConcurrentLinkedQueue<>(); // event list is ordered by production time
@ReadOperation
public List<Event> eventsPublished() {
return eventsPublished.stream().toList();
}
@ReadOperation
public List<Event> eventsPublishedFilteredBy(@Selector Identifiable.Id correlationId) {
return eventsPublished.stream().filter(event -> event.correlationId().equals(correlationId)).toList();
}
@Override
public void notify(final Event event) {// might want to use a generic subscriber
eventsPublished.add(event);
}
}

View file

@ -0,0 +1,106 @@
package de.accso.flexinale.besucherportal.infrastructure;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import de.accso.flexinale.besucherportal.api_in.event.*;
import de.accso.flexinale.besucherportal.api_out.event.GutscheinEinloesenBeauftragtPublisher;
import de.accso.flexinale.besucherportal.application.services.*;
import de.accso.flexinale.common.application.Config;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile({ "!testdata-backoffice & !testdata-ticketing" })
public class FlexinaleBesucherPortalSpringFactory {
// Backoffice
// Film
@Bean
public FilmService createBesucherPortalFilmService(final FKKsVTInMemoryCache cache,
final VorfuehrungService vorfuehrungService,
final Config config) {
return new FilmService(cache, vorfuehrungService, config.getMinZeitZwischenVorfuehrungenInMinuten());
}
@Bean
public FilmSubscriber createBesucherPortalFilmSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) {
return new FilmSubscriber(eventBusFactory, cache, endpointEventsConsumed);
}
// Kino
@Bean
public KinoService createBesucherPortalBesucherPortalKinoService(final FKKsVTInMemoryCache cache) {
return new KinoService(cache);
}
@Bean
public KinoSubscriber createBesucherPortalKinoSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) {
return new KinoSubscriber(eventBusFactory, cache, endpointEventsConsumed);
}
// Vorfuehrung
@Bean
public VorfuehrungService createBesucherPortalVorfuehrungService() {
return new VorfuehrungService();
}
@Bean
public VorfuehrungSubscriber createBesucherPortalVorfuehrungSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) {
return new VorfuehrungSubscriber(eventBusFactory, cache, endpointEventsConsumed);
}
// Ticket
@Bean
public TicketSubscriber createBesucherPortalTicketGekauftSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) {
return new TicketSubscriber(eventBusFactory, cache, endpointEventsConsumed);
}
@Bean
public TicketService createBesucherPortalTicketService(
final GutscheinEinloesenBeauftragtPublication gutscheinEinloesenBeauftragtPublisher) {
return new TicketService(gutscheinEinloesenBeauftragtPublisher);
}
@Bean
public GutscheinEinloesenBeauftragtPublication createBesucherPortalGutscheinEinloesenBeauftragtPublisher(
final EventBusFactory eventBusFactory,
final FlexinaleBesucherPortalActuatorEndpointEventsPublished endpointEventsPublished) {
return new GutscheinEinloesenBeauftragtPublisher(eventBusFactory, endpointEventsPublished);
}
// Kontingent
@Bean
public KontingentSubscriber createBesucherPortalKontingentSubscriber(final EventBusFactory eventBusFactory,
final FKKsVTInMemoryCache cache,
final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) {
return new KontingentSubscriber(eventBusFactory, cache, endpointEventsConsumed);
}
// ------------------------------------------------------------------------------------------------
// Cache
@Bean
public FKKsVTInMemoryCache createBesucherPortalFKKsVTInMemoryCache() {
return new FKKsVTInMemoryCache();
}
// ------------------------------------------------------------------------------------------------
// Actuator Event serialization
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonBesucherPortalCustomizer() {
return builder -> builder.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
}

View file

@ -0,0 +1,74 @@
application.title=FLEXinale as Distributed Services, Besucherportal
application.version=@pom.version@ @maven.build.timestamp@
spring.banner.location=classpath:/flexinale-banner.txt
#########################################################################
# For endpoint /version
#########################################################################
build.version=@pom.version@
build.date=@maven.build.timestamp@
#########################################################################
# Persistence
#########################################################################
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-besucherportal
spring.datasource.name=flexinale
spring.datasource.username=flexinale
spring.datasource.password=flexinale
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database=postgresql
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.generate-ddl=true
# uses schema if existing (or creates anew)
# in a real production environment one should use 'validate' which does just validation but doesn't change anything
spring.jpa.hibernate.ddl-auto=update
# Pros and Cons: See https://www.baeldung.com/spring-open-session-in-view,
# https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot
spring.jpa.open-in-view=false
# spring.jpa.show-sql=true
#########################################################################
# Web
#########################################################################
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
server.error.path=/error
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always
server.error.whitelabel.enabled=false
server.port=8080
#########################################################################
# Security
#########################################################################
security.enable-csrf=true
#########################################################################
# Kafka
#########################################################################
spring.kafka.consumer.group-id=flexinale-distributed-besucherportal
# Spring Kafka Consumer
spring.kafka.consumer.bootstrap-servers=localhost:29092
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
# Spring Kafka Producer
spring.kafka.producer.bootstrap-servers=localhost:29092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
#########################################################################
# Metrics endpoints, micrometer/prometheus/grafana
#########################################################################
# enable and expose
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false
#########################################################################
# flexinale properties
#########################################################################
# time in minutes
de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=30

View file

@ -0,0 +1,10 @@
-------------------------------------------------------------------------
,------. ,--. ,------. ,--. ,--. ,--. ,--.
| .---' | | | .---' \ `.' / `--' ,--,--, ,--,--. | | ,---.
| `--, | | | `--, .' \ ,--. | \ ' ,-. | | | | .-. :
| |` | '--. | `---. / .'. \ | | | || | \ '-' | | | \ --.
`--' `-----' `------' '--' '--' `--' `--''--' `--`--' `--' `----'
${application.title} on port ${server.port}
(version ${application.version})
Powered by Spring Boot ${spring-boot.version}
-------------------------------------------------------------------------

View file

@ -0,0 +1,20 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.orm.jpa" level="INFO"/>
<logger name="org.springframework.boot.autoconfigure.domain.EntityScan" level="INFO"/>
<logger name="org.apache.kafka" level="WARN"/>
<logger name="org.apache.kafka.clients.admin.AdminClient" level="INFO"/>
<logger name="org.apache.kafka.clients.consumer.ConsumerConfig" level="INFO"/>
<logger name="org.apache.kafka.clients.producer.ProducerConfig" level="INFO"/>
<logger name="de.accso" level="INFO"/>
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View file

@ -0,0 +1,44 @@
<!--
Copyright 2016 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Clear Cache</title>
</head>
<body>
<button id="button">Clear</button>
<script>
button.addEventListener('click', function() {
navigator.serviceWorker && navigator.serviceWorker.register('/sw.js').then(function(registration) {
caches.delete('your-magic-cache').then(() => {
alert('ok');
}, () => {
alert('Failed!');
});
}).catch(err => {
alert('Couldn\'t get Service Worker- is it installed?');
});
});
</script>
</body>
</html>

View file

@ -0,0 +1,92 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
function urlB64ToUint8Array(base64String) {
const suffixLength = 4 - base64String.length % 4;
const base64 = (base64String + '='.repeat(suffixLength))
.replace(/\-/g, '+')
.replace(/_/g, '/');
const raw = window.atob(base64);
const output = new Uint8Array(raw.length);
Array.from(raw).forEach((c, i) => output[i] = c.charCodeAt(0));
return output;
}
function stringToBase64Url(s) {
const base64 = window.btoa(s);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
}
function JSONToBase64Url(data) {
const s = JSON.stringify(data);
return stringToBase64Url(s);
}
function uint8ArrayToBase64Url(array) {
const s = String.fromCodePoint(...array);
return stringToBase64Url(s);
}
/**
* @param {string} publicKey public key in URL-safe base64
* @param {string} privateKey private key in URL-safe base64
* @param {string} endpoint from PushSubscription
* @param {string} sender either "mailto:<email>" or a web address
* @return {!Promise<string>} the Authorization header
*/
function prepareAuthorization(publicKey, privateKey, endpoint, sender) {
const origin = new URL(endpoint).origin;
const defaultExpiration = Math.floor(Date.now() / 1000) + 43200; // 12 hours in future
const header = {
typ: 'JWT',
alg: 'ES256'
};
const jwtPayload = {
aud: origin,
exp: defaultExpiration,
sub: sender,
};
// unsignedToken is the URL-safe base64 encoded JSON header and body joined by a dot.
const unsignedToken = JSONToBase64Url(header) + '.' + JSONToBase64Url(jwtPayload);
// Sign unsignedToken using ES256 (SHA-256 over ECDSA). This requires the private key.
const publicKeyArray = urlB64ToUint8Array(publicKey);
const key = {
kty: 'EC',
crv: 'P-256',
x: uint8ArrayToBase64Url(publicKeyArray.subarray(1, 33)),
y: uint8ArrayToBase64Url(publicKeyArray.subarray(33, 65)),
d: privateKey,
};
// Perform the signing. importKey returns a Promise, so wait for it to finish.
const args = {name: 'ECDSA', namedCurve: 'P-256'};
return crypto.subtle.importKey('jwk', key, args, true, ['sign'])
.then(key => {
return crypto.subtle.sign({
name: 'ECDSA',
hash: {
name: 'SHA-256',
},
}, key, (new TextEncoder('utf-8')).encode(unsignedToken));
})
.then(buffer => new Uint8Array(buffer))
.then(signature => {
return 'WebPush ' + unsignedToken + '.' + uint8ArrayToBase64Url(signature);
});
}

View file

@ -0,0 +1,82 @@
.header {
margin: 15px;
}
.nav.black {
background-color: #000000;
}
.FLEXnav.distributed {
background-color: darkseagreen;
}
.container {
margin: 15px;
font-size: 85%;
}
.FLEXmain {
margin: 15px;
font-size: 500%;
font-weight: bold;
}
.FLEXmainSmall {
margin: 15px;
font-size: 200%;
font-weight: bold;
color:green;
}
.FLEXerror {
margin: 15px;
font-size: 500%;
font-weight: bold;
color: red;
}
.brand.link {
color: #ffffff;
font-weight: normal;
font-size: large;
}
.brand.day {
color: #ff0000;
font-weight: normal;
font-size: large;
}
.FLEXfooter {
margin: 25px;
font-size: small;
}
.FLEXlist {
width: 100%;
table-layout: fixed;
}
.FLEXdetails {
width: 100%;
table-layout: fixed;
}
.FLEXid {
width: 12%;
min-width: 10px;
}
.FLEXstring {
width: 22%;
min-width: 32px;
}
.FLEXstring.red {
color: red;
}
.FLEXlink {
width: 10%;
min-width: 20px;
}
.FLEXbutton {
min-width: 102px;
}
.FLEXbutton.red {
background-color: #ff4136;
}
.FLEXbutton.green {
background-color: #2ecc40;
}
.FLEXbutton.lightblue {
background-color: #5897fb;
}

View file

@ -0,0 +1,443 @@
/*!
* jQuery UI CSS Framework 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/theming/
*
* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
*/
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Arial,Helvetica,sans-serif;
font-size: 1em;
}
.ui-widget .ui-widget {
font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Arial,Helvetica,sans-serif;
font-size: 1em;
}
.ui-widget.ui-widget-content {
border: 1px solid #c5c5c5;
}
.ui-widget-content {
border: 1px solid #dddddd;
background: #ffffff;
color: #333333;
}
.ui-widget-content a {
color: #333333;
}
.ui-widget-header {
border: 1px solid #dddddd;
background: #e9e9e9;
color: #333333;
font-weight: bold;
}
.ui-widget-header a {
color: #333333;
}
/* Interaction states
----------------------------------*/
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default,
.ui-button,
/* We use html here because we need a greater specificity to make sure disabled
works properly when clicked or hovered */
html .ui-button.ui-state-disabled:hover,
html .ui-button.ui-state-disabled:active {
border: 1px solid #c5c5c5;
background: #f6f6f6;
font-weight: normal;
color: #454545;
}
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited,
a.ui-button,
a:link.ui-button,
a:visited.ui-button,
.ui-button {
color: #454545;
text-decoration: none;
}
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
.ui-state-focus,
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus,
.ui-button:hover,
.ui-button:focus {
border: 1px solid #cccccc;
background: #ededed;
font-weight: normal;
color: #2b2b2b;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited,
.ui-state-focus a,
.ui-state-focus a:hover,
.ui-state-focus a:link,
.ui-state-focus a:visited,
a.ui-button:hover,
a.ui-button:focus {
color: #2b2b2b;
text-decoration: none;
}
.ui-visual-focus {
box-shadow: 0 0 3px 1px rgb(94, 158, 214);
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active,
a.ui-button:active,
.ui-button:active,
.ui-button.ui-state-active:hover {
border: 1px solid #003eff;
background: #007fff;
font-weight: normal;
color: #ffffff;
}
.ui-icon-background,
.ui-state-active .ui-icon-background {
border: #003eff;
background-color: #ffffff;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
color: #ffffff;
text-decoration: none;
}
/* Interaction Cues
----------------------------------*/
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
border: 1px solid #dad55e;
background: #fffa90;
color: #777620;
}
.ui-state-checked {
border: 1px solid #dad55e;
background: #fffa90;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
color: #777620;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
border: 1px solid #f1a899;
background: #fddfdf;
color: #5f3f3f;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
color: #5f3f3f;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
color: #5f3f3f;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
opacity: .7;
filter:Alpha(Opacity=70); /* support: IE8 */
font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
opacity: .35;
filter:Alpha(Opacity=35); /* support: IE8 */
background-image: none;
}
.ui-state-disabled .ui-icon {
filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
}
/* Icons
----------------------------------*/
/* states and images */
.ui-icon {
width: 16px;
height: 16px;
}
.ui-icon,
.ui-widget-content .ui-icon {
background-image: url("../../images/ui-icons_444444_256x240.png");
}
.ui-widget-header .ui-icon {
background-image: url("../../images/ui-icons_444444_256x240.png");
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon,
.ui-button:hover .ui-icon,
.ui-button:focus .ui-icon {
background-image: url("../../images/ui-icons_555555_256x240.png");
}
.ui-state-active .ui-icon,
.ui-button:active .ui-icon {
background-image: url("../../images/ui-icons_ffffff_256x240.png");
}
.ui-state-highlight .ui-icon,
.ui-button .ui-state-highlight.ui-icon {
background-image: url("../../images/ui-icons_777620_256x240.png");
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
background-image: url("../../images/ui-icons_cc0000_256x240.png");
}
.ui-button .ui-icon {
background-image: url("../../images/ui-icons_777777_256x240.png");
}
/* positioning */
.ui-icon-blank { background-position: 16px 16px; }
.ui-icon-caret-1-n { background-position: 0 0; }
.ui-icon-caret-1-ne { background-position: -16px 0; }
.ui-icon-caret-1-e { background-position: -32px 0; }
.ui-icon-caret-1-se { background-position: -48px 0; }
.ui-icon-caret-1-s { background-position: -65px 0; }
.ui-icon-caret-1-sw { background-position: -80px 0; }
.ui-icon-caret-1-w { background-position: -96px 0; }
.ui-icon-caret-1-nw { background-position: -112px 0; }
.ui-icon-caret-2-n-s { background-position: -128px 0; }
.ui-icon-caret-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -65px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -65px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
border-top-left-radius: 3px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
border-top-right-radius: 3px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
border-bottom-left-radius: 3px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
border-bottom-right-radius: 3px;
}
/* Overlays */
.ui-widget-overlay {
background: #aaaaaa;
opacity: .3;
filter: Alpha(Opacity=30); /* support: IE8 */
}
.ui-widget-shadow {
-webkit-box-shadow: 0px 0px 5px #666666;
box-shadow: 0px 0px 5px #666666;
}

View file

@ -0,0 +1,448 @@
/*
* Table styles
*/
table.dataTable {
width: 100%;
margin: 0 auto;
clear: both;
border-collapse: separate;
border-spacing: 0;
/*
* Header and footer styles
*/
/*
* Body styles
*/
}
table.dataTable thead th,
table.dataTable tfoot th {
font-weight: bold;
}
table.dataTable thead th,
table.dataTable thead td {
padding: 10px 18px;
border-bottom: 1px solid #111;
}
table.dataTable thead th:active,
table.dataTable thead td:active {
outline: none;
}
table.dataTable tfoot th,
table.dataTable tfoot td {
padding: 10px 18px 6px 18px;
border-top: 1px solid #111;
}
table.dataTable thead .sorting,
table.dataTable thead .sorting_asc,
table.dataTable thead .sorting_desc,
table.dataTable thead .sorting_asc_disabled,
table.dataTable thead .sorting_desc_disabled {
cursor: pointer;
*cursor: hand;
background-repeat: no-repeat;
background-position: center right;
}
table.dataTable thead .sorting {
background-image: url("../../images/sort_both.png");
}
table.dataTable thead .sorting_asc {
background-image: url("../../images/sort_asc.png");
}
table.dataTable thead .sorting_desc {
background-image: url("../../images/sort_desc.png");
}
table.dataTable thead .sorting_asc_disabled {
background-image: url("../../images/sort_asc_disabled.png");
}
table.dataTable thead .sorting_desc_disabled {
background-image: url("../../images/sort_desc_disabled.png");
}
table.dataTable tbody tr {
background-color: #ffffff;
}
table.dataTable tbody tr.selected {
background-color: #B0BED9;
}
table.dataTable tbody th,
table.dataTable tbody td {
padding: 8px 10px;
}
table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
border-top: 1px solid #ddd;
}
table.dataTable.row-border tbody tr:first-child th,
table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
table.dataTable.display tbody tr:first-child td {
border-top: none;
}
table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
border-top: 1px solid #ddd;
border-right: 1px solid #ddd;
}
table.dataTable.cell-border tbody tr th:first-child,
table.dataTable.cell-border tbody tr td:first-child {
border-left: 1px solid #ddd;
}
table.dataTable.cell-border tbody tr:first-child th,
table.dataTable.cell-border tbody tr:first-child td {
border-top: none;
}
table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
background-color: #f9f9f9;
}
table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
background-color: #acbad4;
}
table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
background-color: #f6f6f6;
}
table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
background-color: #aab7d1;
}
table.dataTable.order-column tbody tr > .sorting_1,
table.dataTable.order-column tbody tr > .sorting_2,
table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
table.dataTable.display tbody tr > .sorting_2,
table.dataTable.display tbody tr > .sorting_3 {
background-color: #fafafa;
}
table.dataTable.order-column tbody tr.selected > .sorting_1,
table.dataTable.order-column tbody tr.selected > .sorting_2,
table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
table.dataTable.display tbody tr.selected > .sorting_2,
table.dataTable.display tbody tr.selected > .sorting_3 {
background-color: #acbad5;
}
table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
background-color: #f1f1f1;
}
table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
background-color: #f3f3f3;
}
table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
background-color: whitesmoke;
}
table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
background-color: #a6b4cd;
}
table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
background-color: #a8b5cf;
}
table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
background-color: #a9b7d1;
}
table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
background-color: #fafafa;
}
table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
background-color: #fcfcfc;
}
table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
background-color: #fefefe;
}
table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
background-color: #acbad5;
}
table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
background-color: #aebcd6;
}
table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
background-color: #afbdd8;
}
table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
background-color: #eaeaea;
}
table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
background-color: #ececec;
}
table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
background-color: #efefef;
}
table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
background-color: #a2aec7;
}
table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
background-color: #a3b0c9;
}
table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
background-color: #a5b2cb;
}
table.dataTable.no-footer {
border-bottom: 1px solid #111;
}
table.dataTable.nowrap th, table.dataTable.nowrap td {
white-space: nowrap;
}
table.dataTable.compact thead th,
table.dataTable.compact thead td {
padding: 4px 17px 4px 4px;
}
table.dataTable.compact tfoot th,
table.dataTable.compact tfoot td {
padding: 4px;
}
table.dataTable.compact tbody th,
table.dataTable.compact tbody td {
padding: 4px;
}
table.dataTable th.dt-left,
table.dataTable td.dt-left {
text-align: left;
}
table.dataTable th.dt-center,
table.dataTable td.dt-center,
table.dataTable td.dataTables_empty {
text-align: center;
}
table.dataTable th.dt-right,
table.dataTable td.dt-right {
text-align: right;
}
table.dataTable th.dt-justify,
table.dataTable td.dt-justify {
text-align: justify;
}
table.dataTable th.dt-nowrap,
table.dataTable td.dt-nowrap {
white-space: nowrap;
}
table.dataTable thead th.dt-head-left,
table.dataTable thead td.dt-head-left,
table.dataTable tfoot th.dt-head-left,
table.dataTable tfoot td.dt-head-left {
text-align: left;
}
table.dataTable thead th.dt-head-center,
table.dataTable thead td.dt-head-center,
table.dataTable tfoot th.dt-head-center,
table.dataTable tfoot td.dt-head-center {
text-align: center;
}
table.dataTable thead th.dt-head-right,
table.dataTable thead td.dt-head-right,
table.dataTable tfoot th.dt-head-right,
table.dataTable tfoot td.dt-head-right {
text-align: right;
}
table.dataTable thead th.dt-head-justify,
table.dataTable thead td.dt-head-justify,
table.dataTable tfoot th.dt-head-justify,
table.dataTable tfoot td.dt-head-justify {
text-align: justify;
}
table.dataTable thead th.dt-head-nowrap,
table.dataTable thead td.dt-head-nowrap,
table.dataTable tfoot th.dt-head-nowrap,
table.dataTable tfoot td.dt-head-nowrap {
white-space: nowrap;
}
table.dataTable tbody th.dt-body-left,
table.dataTable tbody td.dt-body-left {
text-align: left;
}
table.dataTable tbody th.dt-body-center,
table.dataTable tbody td.dt-body-center {
text-align: center;
}
table.dataTable tbody th.dt-body-right,
table.dataTable tbody td.dt-body-right {
text-align: right;
}
table.dataTable tbody th.dt-body-justify,
table.dataTable tbody td.dt-body-justify {
text-align: justify;
}
table.dataTable tbody th.dt-body-nowrap,
table.dataTable tbody td.dt-body-nowrap {
white-space: nowrap;
}
table.dataTable,
table.dataTable th,
table.dataTable td {
box-sizing: content-box;
}
/*
* Control feature layout
*/
.dataTables_wrapper {
position: relative;
clear: both;
*zoom: 1;
zoom: 1;
}
.dataTables_wrapper .dataTables_length {
float: left;
}
.dataTables_wrapper .dataTables_filter {
float: right;
text-align: right;
}
.dataTables_wrapper .dataTables_filter input {
margin-left: 0.5em;
}
.dataTables_wrapper .dataTables_info {
clear: both;
float: left;
padding-top: 0.755em;
}
.dataTables_wrapper .dataTables_paginate {
float: right;
text-align: right;
padding-top: 0.25em;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
box-sizing: border-box;
display: inline-block;
min-width: 1.5em;
padding: 0.5em 1em;
margin-left: 2px;
text-align: center;
text-decoration: none !important;
cursor: pointer;
*cursor: hand;
color: #333 !important;
border: 1px solid transparent;
border-radius: 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
color: #333 !important;
border: 1px solid #979797;
background-color: white;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
/* IE10+ */
background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
/* W3C */
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
cursor: default;
color: #666 !important;
border: 1px solid transparent;
background: transparent;
box-shadow: none;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
color: white !important;
border: 1px solid #111;
background-color: #585858;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, #585858 0%, #111 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, #585858 0%, #111 100%);
/* IE10+ */
background: -o-linear-gradient(top, #585858 0%, #111 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #585858 0%, #111 100%);
/* W3C */
}
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
outline: none;
background-color: #2b2b2b;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* IE10+ */
background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
/* W3C */
box-shadow: inset 0 0 3px #111;
}
.dataTables_wrapper .dataTables_paginate .ellipsis {
padding: 0 1em;
}
.dataTables_wrapper .dataTables_processing {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 40px;
margin-left: -50%;
margin-top: -25px;
padding-top: 20px;
text-align: center;
font-size: 1.2em;
background-color: white;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
}
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_processing,
.dataTables_wrapper .dataTables_paginate {
color: #333;
}
.dataTables_wrapper .dataTables_scroll {
clear: both;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
*margin-top: -1px;
-webkit-overflow-scrolling: touch;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
vertical-align: middle;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
height: 0;
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
.dataTables_wrapper.no-footer .dataTables_scrollBody {
border-bottom: 1px solid #111;
}
.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,
.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
border-bottom: none;
}
.dataTables_wrapper:after {
visibility: hidden;
display: block;
content: "";
clear: both;
height: 0;
}
@media screen and (max-width: 767px) {
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate {
float: none;
text-align: center;
}
.dataTables_wrapper .dataTables_paginate {
margin-top: 0.5em;
}
}
@media screen and (max-width: 640px) {
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
float: none;
text-align: center;
}
.dataTables_wrapper .dataTables_filter {
margin-top: 0.5em;
}
}

View file

@ -0,0 +1,57 @@
/*
* Timepicker stylesheet
* Highly inspired from datepicker
* FG - Nov 2010 - Web3R
*
* version 0.0.3 : Fixed some settings, more dynamic
* version 0.0.4 : Removed width:100% on tables
* version 0.1.1 : set width 0 on tables to fix an ie6 bug
*/
.ui-timepicker-inline { display: inline; }
#ui-timepicker-div { padding: 0.2em; }
.ui-timepicker-table { display: inline-table; width: 0; }
.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; }
.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; }
.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; }
.ui-timepicker-table td { padding: 0.1em; width: 2.2em; }
.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; }
/* span for disabled cells */
.ui-timepicker-table td span {
display:block;
padding:0.2em 0.3em 0.2em 0.5em;
width: 1.2em;
text-align:right;
text-decoration:none;
}
/* anchors for clickable cells */
.ui-timepicker-table td a {
display:block;
padding:0.2em 0.3em 0.2em 0.5em;
/*width: 1.2em;*/
cursor: pointer;
text-align:right;
text-decoration:none;
}
/* buttons and button pane styling */
.ui-timepicker .ui-timepicker-buttonpane {
background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0;
}
.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
/* The close button */
.ui-timepicker .ui-timepicker-close { float: right }
/* the now button */
.ui-timepicker .ui-timepicker-now { float: left; }
/* the deselect button */
.ui-timepicker .ui-timepicker-deselect { float: left; }

View file

@ -0,0 +1,82 @@
<!--
Copyright 2016 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dragotchi</title>
<link rel="stylesheet" type="text/css" href="styles.css" media="screen" />
<script defer src="crypto.js"></script>
<script defer src="site.js"></script>
<script defer src="dragon.js"></script>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel="manifest" href="manifest.json" />
</head>
<body>
<header>
<img class="logo" src="logo.png" />
<h1>Dragotchi</h1>
<ul>
<li><a href="drindex.html">Home</a></li>
<li><a href="dragon.html">Dragotchi</a></li>
<li><a href="faq.html">FAQ</a></li>
</ul>
</header>
<hr />
<section>
<div class="dragon" id="outer">
<div id="guy" style="transform: translate(100px, 100px)">
<img src="dragon.png" />
</div>
<div id="gold"></div>
</div>
<table align="center" id="data">
</table>
<form method="post" class="actions" id="actions">
</form>
<script>
var size = outer.getBoundingClientRect();
window.setInterval(function() {
var x = Math.random() * size.width;
var y = Math.random() * size.height;
guy.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
}, 5000);
</script>
</section>
<hr />
<footer>
Learn more at <a href="https://developers.google.com/web">Google Developers</a>
</footer>
</body>
</html>

View file

@ -0,0 +1,91 @@
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
function refresh(action, callback) {
var x = new XMLHttpRequest();
x.onload = function() {
var out = x.response;
if (typeof out == 'string') {
out = JSON.parse(out);
}
callback(out);
};
x.onerror = function() {
alert('couldn\'t fetch dragon status');
};
if (action) {
x.open('POST', 'https://dragon-server.appspot.com/?action=' + action);
} else {
x.open('GET', 'https://dragon-server.appspot.com/');
}
x.send();
}
window.addEventListener('load', function() {
function createDataRow(name, value) {
var row = document.createElement('tr');
var th = document.createElement('th');
th.textContent = name;
row.appendChild(th);
var td = document.createElement('td');
td.textContent = value;
row.appendChild(td);
return row;
}
var timeout;
(function work(action) {
window.clearTimeout(timeout);
timeout = window.setTimeout(work, 60 * 1000); // 60s
if (action) { // clicked action, clear future actions
actions.textContent = '';
}
refresh(action, function(status) {
gold.textContent = '';
for (var i = 0; i < status.Gold; ++i) {
var coin = document.createElement('span');
coin.className = 'coin';
coin.style.top = Math.random() * 100 + '%';
coin.style.left = Math.random() * 100 + '%';
gold.appendChild(coin);
}
data.textContent = '';
data.appendChild(createDataRow('Gold', status.Gold));
data.appendChild(createDataRow('Size', status.Size + 'kg'));
// TO DO: update size
actions.textContent = '';
status.Actions.forEach(function(action) {
var button = document.createElement('button');
button.addEventListener('click', function(ev) {
ev.preventDefault();
work(action.ID);
});
button.textContent = action.Name;
actions.appendChild(button);
});
});
}());
});

View file

@ -0,0 +1,85 @@
<!--
Copyright 2016 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dragotchi</title>
<link rel="stylesheet" type="text/css" href="styles.css" media="screen" />
<script defer src="crypto.js"></script>
<script defer src="site.js"></script>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel="manifest" href="manifest.json" />
</head>
<body>
<header>
<img class="logo" src="logo.png" />
<h1>Dragotchi</h1>
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="dragon.html">Dragotchi</a></li>
<li><a href="faq.html">FAQ</a></li>
</ul>
</header>
<hr />
<section>
<h2>Home</h2>
<p>
Click and play with your very own Dragon! Feed it gold and watch it grow over time.
</p>
<p>
Soon it will be an excited little dragon eating villagers and burning houses down.
</p>
<p>
To get started, click on the <a href="dragon.html">Dragotchi link</a>!
</p>
<img src="construction.gif" width="69" />
<p>
Dragotchi is always <blink><strong>Under Construction</strong></blink>!!!
</p>
</section>
<hr />
<footer>
Learn more at <a href="https://developers.google.com/web">Google Developers</a>
</footer>
<div class="counter">
<span>0</span>
<span>0</span>
<span>0</span>
<span>2</span>
<span>0</span>
<span>1</span>
<span>6</span>
</div>
</body>
</html>

View file

@ -0,0 +1,70 @@
<!--
Copyright 2016 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dragotchi</title>
<link rel="stylesheet" type="text/css" href="styles.css" media="screen" />
<script defer src="crypto.js"></script>
<script defer src="site.js"></script>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel="manifest" href="manifest.json" />
</head>
<body>
<header>
<img class="logo" src="logo.png" />
<h1>Dragotchi</h1>
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="dragon.html">Dragotchi</a></li>
<li><a href="faq.html">FAQ</a></li>
</ul>
</header>
<hr />
<section>
<h2>FAQ</h2>
<dl>
<dt>Are you serious?</dt>
<dd>Yes.</dd>
<dt>How cool are dragons?</dt>
<dd>Well, it's not clear whether they are cold- or warm-blooded.</dd>
<dt>Why does this exist?</dt>
<dd>This site has been built as an example for <a href="https://codelabs.developers.google.com/">Google Codelabs</a>.</dd>
<dt>Where are the images from?</dt>
<dd>The logo is from <a href="https://maps.googleblog.com/2012/03/begin-your-quest-with-google-maps-8-bit.html">Google Maps 8-bit</a>, the "Under Construction" gif is generally available online, some assets from <a href="https://openclipart.org/detail/190725/chinese-zodiac-dragon">openclipart</a>, other assets were built for this site.</dd>
</dl>
</section>
<hr />
<footer>
Learn more at <a href="https://developers.google.com/web">Google Developers</a>
</footer>
</body>
</html>

View file

@ -0,0 +1,15 @@
{
"name": "The Most Awesome Dragon Site",
"short_name": "🐉🐉🐉",
"display": "minimal-ui",
"start_url": "/",
"theme_color": "#673ab6",
"background_color": "#111111",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}

View file

@ -0,0 +1,17 @@
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
navigator.serviceWorker && navigator.serviceWorker.register('./sw.js').then(function(registration) {
console.log('Excellent, registered with scope: ', registration.scope);
});

View file

@ -0,0 +1,161 @@
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
body {
background: black url(background.jpeg) repeat-x center top;
background-size: 200px;
color: white;
text-align: center;
}
a {
color: #ff9249;
}
/** I'm so sorry. */
blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
67% { opacity: 0 }
}
header .logo {
width: 100px;
}
header ul {
list-style: none;
padding: 0;
color: #555;
}
header li {
display: inline-block;
}
header li::before {
content: " — ";
}
header li:first-child::before {
content: "";
}
.counter {
display: inline-flex;
margin: 20px;
}
.counter span {
border: 1px solid #333;
width: 12px;
font-family: Monospace;
background: linear-gradient(to top, red, blue);
}
dl {
width: 400px;
margin: 0 auto;
text-align: left;
}
dd {
margin-bottom: 1em;
}
section table {
margin: 0 auto;
text-align: left;
}
section table td,
section table th {
min-width: 50px;
}
.dragon {
box-sizing: border-box;
padding: 20px;
width: 280px;
height: 280px;
margin: 12px auto;
border: 2px solid rgba(255, 255, 255, 0.125);
background: #111;
border-radius: 2px;
position: relative;
overflow: hidden;
}
.dragon #guy {
position: absolute;
width: 100px;
margin-left: -50px;
margin-top: -50px;
z-index: 1000;
transition: transform 1s ease-in-out;
}
.dragon img {
width: 100%;
cursor: pointer;
transition: transform 0.25s;
}
.dragon img:hover {
transform: scale(1.45) rotate(10deg);
}
.coin {
position: absolute;
opacity: 0.5;
width: 10px;
height: 5px;
margin-left: -5px;
margin-top: -2px;
background: yellow;
border-radius: 100%; /** oval */
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
border-top: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.75), 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.actions {
width: 280px;
height: 80px;
box-sizing: border-box;
border: 2px solid rgba(255, 255, 255, 0.125);
background: #211;
border-radius: 2px;
margin: 12px auto;
display: flex;
padding: 2px;
}
.actions button {
border: 0;
flex-grow: 1;
width: 300%; /** forces equal size */
margin-left: 2px;
background: rgba(255, 255, 255, 0.125);
color: white;
font-size: 16px;
padding: 12px;
opacity: 0.8;
}
.actions button:first-child {
margin-left: 0;
}
.actions button:disabled {
text-decoration: line-through;
}
.actions button:hover:not(:disabled) {
opacity: 1.0;
cursor: pointer;
}

View file

@ -0,0 +1,51 @@
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('your-magic-cache').then(function(cache) {
return cache.addAll([
'/',
'/drindex.html',
'/dragon.html',
'/faq.html',
'/manifest.json',
'/background.jpeg',
'/construction.gif',
'/dragon.png',
'/logo.png',
'/site.js',
'/dragon.js',
'/styles.css',
]);
})
);
});
self.addEventListener('fetch', function(event) {
if (event.request.url == 'https://dragon-server.appspot.com/') {
console.info('responding to dragon-server fetch with Service Worker! 🤓');
event.respondWith(fetch(event.request).catch(function(e) {
let out = {Gold: 1, Size: -1, Actions: []};
return new Response(JSON.stringify(out));
}));
return;
}
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale - Error</title>
<link href="icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{../css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{../css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="FLEXerror">
<table>
<td>Error</td>
<td><img height="75%" src="../icons/movies.png" width="75%"></td>
</table>
</div>
<div>
<table>
<tr>
<td>Timestamp</td>
<td th:text="${timestamp}"/>
</tr>
<tr>
<td>Path</td>
<td th:text="${path}"/>
</tr>
<tr>
<td>Error</td>
<td th:text="${error}"/>
</tr>
<tr>
<td>Status</td>
<td th:text="${status}"/>
</tr>
<tr>
<td>Message</td>
<td th:text="${message}"/>
</tr>
<tr>
<td>Exception</td>
<td th:text="${exception}"/>
</tr>
<tr>
<td>Trace</td>
<td>
<pre th:text="${trace}"/>
</td>
</tr>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale - Film Details</title>
<link href="../icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="FLEXerrorSmall" role="alert" th:text="${error}" th:if="${error}"></div>
<div class="container" th:if="${film != null}">
<h1>
<th:block th:text="${film.titel.raw()}"/>
</h1>
<table class="FLEXdetails">
<tr>
<th class="FLEXid">ID</th>
<th class="FLEXstring">Titel</th>
<th class="FLEXlink">Link</th>
<th class="FLEXString">Dauer [m]</th>
</tr>
<tr>
<td th:text="${film.id.id()}"></td>
<td th:text="${film.titel.raw()}"></td>
<td th:if="${film.imdbUrl.raw() != null}">
<a th:href="${film.imdbUrl.raw()}">IMDB Link</a>
</td>
<td th:text="${film.dauerInMinuten.raw()}"></td>
</tr>
</table>
</div>
<div class="container" th:if="${vorfuehrungenMitRestkontingent.size() != 0}">
<h2>Vorführungen</h2>
<table class="FLEXlist">
<tr>
<th class="FLEXid">ID</th>
<th class="FLEXstring">Zeit</th>
<th class="FLEXstring">Kino</th>
<th class="FLEXstring">Saal</th>
<th class="FLEXstring">Tickets</th>
</tr>
<tr th:each="vorfuehrungMitRestkontingent : ${vorfuehrungenMitRestkontingent}">
<td>
<th:block th:text="${vorfuehrungMitRestkontingent.vorfuehrungId().id()}"/>
</td>
<td>
<th:block th:text="${#temporals.format(vorfuehrungMitRestkontingent.zeit().raw(), 'dd.MM.yyyy')}"/>
,
<th:block
th:text="${T(de.accso.flexinale.common.shared_kernel.TimeFormatter).calculateString(vorfuehrungMitRestkontingent.zeit().raw(), film.dauerInMinuten.raw())}"/>
Uhr
</td>
<td>
<a th:href="@{/kino/{id}(id=${vorfuehrungMitRestkontingent.kino().id.id()})}">
<th:block th:text="${vorfuehrungMitRestkontingent.kino().name.raw()}"/>
</a>
</td>
<td>
<th:block th:text="${vorfuehrungMitRestkontingent.kinoSaal().name.raw()}"/>
</td>
<!-- Fallunterscheidungen...-->
<!-- Benutzer hat schon Ticket -->
<td th:if="${vorfuehrungenMitTicket.contains(vorfuehrungMitRestkontingent.vorfuehrungId().id())}">
<a href="tickets" th:href="@{/tickets}">
Du hast bereits Ticket(s).
</a>
</td>
<!-- Benutzer hat noch kein Ticket -->
<!-- ... 1) Keine Tickets mehr verfügbar -->
<td th:if="not ${vorfuehrungenMitTicket.contains(vorfuehrungMitRestkontingent.vorfuehrungId().id())}
and ${vorfuehrungMitRestkontingent.restkontingentOnline().raw()} eq 0 ">
Keine Tickets verfügbar
</td>
<!-- ...2) Tickets verfügbar, aber Überlappende Vorführung -->
<td th:if="not ${vorfuehrungenMitTicket.contains(vorfuehrungMitRestkontingent.vorfuehrungId().id())}
and ${vorfuehrungMitRestkontingent.restkontingentOnline().raw()} gt 0
and ${vorfuehrungenMitUeberlapp.contains(vorfuehrungMitRestkontingent.vorfuehrungId().id())}">
<a href="tickets" th:href="@{/tickets}">
Du hast zu der Zeit bereits eine andere Vorführung.
</a>
</td>
<!-- ...3) Tickets verfügbar, keine Überlappende Vorführung, also Gutschein einlösbar-->
<td th:unless=" ${vorfuehrungenMitTicket.contains(vorfuehrungMitRestkontingent.vorfuehrungId().id())}
or ${vorfuehrungMitRestkontingent.restkontingentOnline().raw()} eq 0
or ${vorfuehrungenMitUeberlapp.contains(vorfuehrungMitRestkontingent.vorfuehrungId().id())}">
<form autocomplete="off" method="post" th:action="@{/vorfuehrung/loeseGutscheineOnlineEin}">
<input type="number"
min=1
th:max="${vorfuehrungMitRestkontingent.restkontingentOnline().raw()}<10?${vorfuehrungMitRestkontingent.restkontingentOnline().raw()}:10"
id="anzahl" name="anzahl" placeholder="Gewünschte Anzahl" required="required"/>
<input class="readonly" id="vorfuehrungId1" name="vorfuehrungId" required="required"
th:value="${vorfuehrungMitRestkontingent.vorfuehrungId().id()}" type="hidden"/>
<input class="readonly" id="filmId" name="filmId" required="required"
th:value="${vorfuehrungMitRestkontingent.film().id.id()}" type="hidden"/>
<input class="FLEXbutton " type="submit" value="Gutschein einlösen"/>
</form>
</td>
</tr>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale - Film &Uuml;bersicht</title>
<link href="icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="container" th:if="${#lists.isEmpty(filme)}">
<h1>Film-Programm ist leer!</h1>
</div>
<div class="container" th:if="${not #lists.isEmpty(filme)}">
<h1>Film-Programm</h1>
<table class="FLEXlist">
<tr>
<th class="FLEXstring">Titel</th>
<th class="FLEXlink">Link</th>
</tr>
<tr th:each="film : ${filme}">
<td>
<a th:href="@{film/{id}(id=${film.id.id()})}">
<th:block th:text="${film.titel.raw()}"/>
</a>
</td>
<td th:if="${film.imdbUrl.raw() != null}">
<a target="_blank" th:href="${film.imdbUrl.raw()}">IMDB Link</a>
</td>
</tr>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<title>Flexinale</title>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{../css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{../css/app/main.css}"/>
</head>
<body>
<div class="header" th:fragment="bodyHeader(days)">
<nav class="FLEXnav distributed">
<a class="brand link" href="/" th:href="@{/}">
<span>Flexinale</span>
</a>
<a class="brand link" href="filme" th:href="@{/filme}">
<span>Filme</span>
</a>
<a class="brand link" href="kinos" th:href="@{/kinos}">
<span>Kinos</span>
</a>
<a class="brand link" href="tickets" th:href="@{/tickets}">
<span>Tickets</span>
</a>
<a class="brand link" href="logout" th:href="@{/logout}">
<span>Abmelden</span>
</a>
</nav>
<br>
</div>
<hr>
&nbsp;
<br>
</body>
</html>

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale Distributed - Besucherportal</title>
<link href="icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="FLEXmain">
<table>
<td>Flexinale</td>
<td><img height="125%" src="icons/movies.png" width="125%"></td>
</table>
</div>
<hr>
<div class="FLEXfooter">
<th:block th:text="${applicationTitle}"/>
, Version
<th:block th:text="${buildVersion}"/>
, built on
<th:block th:text="${buildDate}"/>
</div>
</body>
</html>

View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale - Kino Details</title>
<link href="../icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="container" th:if="${kino != null}">
<h1>
<th:block th:text="${kino.name.raw()}"/>
</h1>
<table class="FLEXdetails">
<tr>
<th class="FLEXid">ID</th>
<th class="FLEXstring">Name</th>
<th class="FLEXstring">Adresse</th>
<th class="FLEXlink">Kontakt</th>
</tr>
<tr>
<td th:text="${kino.id.id()}"></td>
<td th:text="${kino.name.raw()}"></td>
<td th:text="${kino.adresse.raw()}"></td>
<td th:text="${kino.emailAdresse.raw()}"></td>
</tr>
</table>
<div class="container" th:if="${kino.kinoSaele.size() != 0}">
<h1>
<th:block th:text="${kino.name.raw()}"/>
Kinosäle
</h1>
<table class="FLEXlist">
<tr>
<th class="FLEXid">ID</th>
<th class="FLEXstring">Name</th>
<th class="FLEXstring">Anzahl Pl&auml;tze</th>
</tr>
<tr th:each="kinoSaal : ${kino.kinoSaele}">
<td>
<th:block th:text="${kinoSaal.id.id()}"/>
</td>
<td>
<th:block th:text="${kinoSaal.name.raw()}"/>
</td>
<td>
<th:block th:text="${kinoSaal.anzahlPlaetze.raw()}"/>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale - Kino &Uuml;bersicht</title>
<link href="icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="container" th:if="${#lists.isEmpty(kinos)}">
<h1>Kino-Liste ist leer!</h1>
</div>
<div class="container" th:if="${not #lists.isEmpty(kinos)}">
<h1>Kino-Liste</h1>
<table class="FLEXlist">
<tr>
<th class="FLEXstring">Name</th>
<th class="FLEXstring">Adresse</th>
<th class="FLEXstring">Anzahl S&auml;le</th>
</tr>
<tr th:each="kino : ${kinos}">
<td>
<a th:href="@{kino/{id}(id=${kino.id.id()})}">
<th:block th:text="${kino.name.raw()}"/>
</a>
</td>
<td>
<th:block th:text="${kino.adresse.raw()}"/>
</td>
<td>
<th:block th:text="${kino.kinoSaele.size()}"/>
</td>
</tr>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Flexinale - Tickets</title>
<link href="icons/movies.png" rel="icon">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link href="../static/css/vendor/picnic.min.css"
media="screen" rel="stylesheet" th:href="@{../css/vendor/picnic.min.css}"/>
<link href="../static/css/app/main.css"
media="screen" rel="stylesheet" th:href="@{../css/app/main.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<div class="FLEXmainSmall" role="alert" th:text="${success}" th:if="${success}"></div>
<div class="container" th:if="${#lists.isEmpty(ticketsByDay)}">
<h1>Keine Tickets vorhanden!</h1>
</div>
<div class="container" th:if="${not #lists.isEmpty(ticketsByDay)}">
<h1>Du hast
<th:block th:text="${totalNumberOfTickets}"/>
Tickets
</h1>
<th:block th:each="ticketListOneDay : ${ticketsByDay}">
<th:block th:if="${not #lists.isEmpty(ticketListOneDay)}">
<h3>
<th:block th:text="${ticketListOneDay.size()}"/>
Vorführung(en)
am
<th:block th:text="${#temporals.format(ticketListOneDay.get(0).zeit().raw(), 'dd.MM.yyyy')}">
</th:block>
</h3>
<table class="FLEXlist">
<tr>
<th class="FLEXstring">Film</th>
<th class="FLEXstring">Vorführung</th>
<th class="FLEXstring">Kino</th>
<th class="FLEXstring">Saal</th>
<th class="FLEXstring">Anzahl gültige Tickets</th>
<th class="FLEXstring">Anzahl ungültige Tickets</th>
</tr>
<tr th:each="vorfuehrungMitAnzahlTickets : ${ticketListOneDay}">
<td>
<a th:href="@{film/{id}(id=${vorfuehrungMitAnzahlTickets.film().id.id()})}">
<th:block th:text="${vorfuehrungMitAnzahlTickets.film().titel.raw()}"/>
</a>
</td>
<td th:text="${T(de.accso.flexinale.common.shared_kernel.TimeFormatter).calculateString(vorfuehrungMitAnzahlTickets.zeit().raw(), vorfuehrungMitAnzahlTickets.film().dauerInMinuten.raw())}"></td>
<td>
<a target="_blank" th:href="@{kino/{id}(id=${vorfuehrungMitAnzahlTickets.kino().id.id()})}">
<th:block th:text="${vorfuehrungMitAnzahlTickets.kino().name.raw()} "/>
</a>
</td>
<td>
<th:block th:text="${vorfuehrungMitAnzahlTickets.kinoSaal().name.raw()}"/>
</td>
<td>
<th:block th:text="${vorfuehrungMitAnzahlTickets.anzahlGueltigeTickets().raw()}"/>
</td>
<td>
<th:block th:text="${vorfuehrungMitAnzahlTickets.anzahlUngueltigeTickets().raw()}"/>
</td>
</tr>
</table>
&nbsp;
&nbsp;
&nbsp;
</th:block>
</th:block>
</div>
</body>

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/besucherportalCacheContents
GET http://localhost:8080/actuator/besucherportalCacheContents
Authorization: Basic admin admin1

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/besucherportalEventsConsumed
GET http://localhost:8080/actuator/besucherportalEventsConsumed
Authorization: Basic admin admin1

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/besucherportalEventsPublished
GET http://localhost:8080/actuator/besucherportalEventsPublished
Authorization: Basic admin admin1

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/health
GET http://localhost:8080/actuator/health
Authorization: Basic admin admin1

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/info
GET http://localhost:8080/actuator/info
Authorization: Basic admin admin1

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/metrics
GET http://localhost:8080/actuator/metrics
Authorization: Basic admin admin1

View file

@ -0,0 +1,3 @@
# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/prometheus
GET http://localhost:8080/actuator/prometheus
Authorization: Basic admin admin1

View file

@ -0,0 +1,21 @@
package de.accso.flexinale;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = {FlexinaleDistributedApplicationBesucherportal.class})
@ActiveProfiles("smoketest")
public class ApplicationPropertiesFileTest {
@Value("${application.version}")
private String propertyApplicationVersion;
@Test
public void testReadsTestPropertiesFile() {
assertThat(propertyApplicationVersion).isEqualTo("test");
}
}

View file

@ -0,0 +1,20 @@
package de.accso.flexinale;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(classes = {FlexinaleDistributedApplicationBesucherportal.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@ActiveProfiles("smoketest")
@DoNotCheckInArchitectureTests
class FlexinaleDistributedApplicationBesucherportalSmokeTest {
@Test
@SuppressWarnings("EmptyMethod")
void testLoadInitialApplicationContext() {
// nope
}
}

View file

@ -0,0 +1,31 @@
package de.accso.flexinale;
import de.accso.flexinale.common.application.Config;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = {FlexinaleDistributedApplicationBesucherportal.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@ActiveProfiles("configtest")
@DoNotCheckInArchitectureTests
class FlexinaleDistributedApplicationBesucherportalSpringConfigTest {
@Autowired
Config config;
@Test
void testLoadSpringConfig() {
// arrange, act
// nope, is loaded auto-magically by Spring, see application-configtest.properties
// assert
assertThat(config.getQuoteOnline()).isEqualTo(33); // default
assertThat(config.getMinZeitZwischenVorfuehrungenInMinuten()).isEqualTo(1234);
}
}

View file

@ -0,0 +1,2 @@
# time in minutes
de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=1234

View file

@ -0,0 +1,39 @@
application.title=FLEXinale as Distributed Services, Besucherportal
application.version=test
spring.banner.location=classpath:/flexinale-banner.txt
#########################################################################
# For endpoint /version
#########################################################################
build.version=@pom.version@
build.date=@maven.build.timestamp@
#########################################################################
# Persistence
#########################################################################
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-besucherportal
spring.datasource.name=flexinale
spring.datasource.username=flexinale
spring.datasource.password=flexinale
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database=postgresql
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.generate-ddl=true
# spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
server.port=9090
#########################################################################
# Web
#########################################################################
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
server.error.path=/error
#########################################################################
# flexinale properties
#########################################################################
# Quote for online kontingent in percent
de.accso.flexinale.kontingent.quote.online=33
# time in minutes
de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=30

View file

@ -0,0 +1,20 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.orm.jpa" level="INFO"/>
<logger name="org.springframework.boot.autoconfigure.domain.EntityScan" level="INFO"/>
<logger name="org.apache.kafka" level="WARN"/>
<logger name="org.apache.kafka.clients.admin.AdminClient" level="INFO"/>
<logger name="org.apache.kafka.clients.consumer.ConsumerConfig" level="INFO"/>
<logger name="org.apache.kafka.clients.producer.ProducerConfig" level="INFO"/>
<logger name="de.accso" level="INFO"/>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>