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,4 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

View file

@ -0,0 +1,6 @@
# flex-training-filmfestival
iSAQB Advanced Level FLEX training: This code contains our case study code for the "filmfestival" example.
This is the main repo containing blueprint and solution.
## Architecture as a monolithic system
This part is in the folder flexinale-monolith

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<!-- We ignore this because all our entities are mapped by Spring Boot magic to Json -->
<Class name="~.*Controller.*" />
<Bug pattern="ENTITY_LEAK"/> <!-- ENTITY_LEAK = Unexpected property could be leaked because a persistence class is directly exposed to the client -->
</Match>
<Match>
<!-- We ignore this in the monolith because we want to make the online quote konfigurable and do not want
the entity Kontingent to be under Spring control. -->
<Class name="de.accso.flexinale.util.Config" />
</Match>
</FindBugsFilter>

View file

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

View file

@ -0,0 +1,6 @@
docker-compose \
--project-name flexinale-monolith \
-f ../../../infrastructure/docker/flexinale-network.yml \
-f ../../../infrastructure/docker/postgres/flexinale-postgres.yml \
-f flexinale-monolith.yml \
up

View file

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

View file

@ -0,0 +1,13 @@
version: '2.2'
services:
flexinale-monolith:
image: de.accso/flexinale-monolith:2024.3.0
depends_on:
- flexinale-postgres
ports:
- "8080:8080"
networks:
- flexinale-network
command: sh -c "java -Dspring.datasource.url=jdbc:postgresql://flexinale-postgres:5432/monolith -jar flexinale-monolith-2024.3.0-spring-boot-fat-jar.jar"

View file

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

View file

@ -0,0 +1,13 @@
version: '2.2'
services:
flexinale-monolith:
image: de.accso/flexinale-monolith:2024.3.0
depends_on:
- flexinale-postgres
ports:
- "8080:8080"
networks:
- flexinale-network
command: sh -c "java -Dspring.datasource.url=jdbc:postgresql://flexinale-postgres:5432/monolith -jar flexinale-monolith-2024.3.0-spring-boot-fat-jar.jar"

View file

@ -0,0 +1,6 @@
podman-compose ^
--project-name flexinale-monolith ^
-f ..\..\..\infrastructure\podman\flexinale-network.yml ^
-f ..\..\..\infrastructure\podman\postgres\flexinale-postgres.yml ^
-f flexinale-monolith.yml ^
up

View file

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

View file

@ -0,0 +1,118 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.accso</groupId>
<artifactId>flexinale</artifactId>
<version>2024.3.0</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>flexinale-monolith</artifactId>
<version>2024.3.0</version>
<name>Flexinale Monolith</name>
<description>Flexinale - FLEX case-study &quot;film festival&quot;, monolithic system</description>
<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.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${org-postgres.version}</version>
<scope>runtime</scope>
</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>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${apache-poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${apache-poi.version}</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>
</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,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - Entities Use Correct Interfaces And Internal StructureTest" type="JUnit" factoryName="JUnit" folderName="ArchitectureTests Monolith">
<module name="flexinale-monolith" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="architecturetests.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="architecturetests" />
<option name="MAIN_CLASS_NAME" value="architecturetests.InternalStructureOfEntityClassesUsingCorrectInterfacesAndAttributeTypesTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - REST upload to add filme" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="REST upload Monolith" path="$PROJECT_DIR$/flexinale-monolith/src/test/curl/rest-post-upload-to-add-and-update-filme.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - REST upload to add kinos" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="REST upload Monolith" path="$PROJECT_DIR$/flexinale-monolith/src/test/curl/rest-post-upload-to-add-and-update-kinos.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - REST upload to add vorfuehrungen" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" folderName="REST upload Monolith" path="$PROJECT_DIR$/flexinale-monolith/src/test/curl/rest-post-upload-to-add-and-update-vorfuehrungen.http" requestIdentifier="#1">
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - delete all data from database" type="JUnit" factoryName="JUnit" folderName="Testdata Monolith">
<module name="flexinale-monolith" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="testdata.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="testdata" />
<option name="MAIN_CLASS_NAME" value="testdata.DatabaseCleaner" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea -Dspring.jpa.hibernate.ddl-auto=create" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - load all data to database" type="JUnit" factoryName="JUnit" folderName="Testdata Monolith">
<module name="flexinale-monolith" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="testdata.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="testdata" />
<option name="MAIN_CLASS_NAME" value="testdata.TestDataLoader" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Monolith - start app (&quot;FlexinaleMonolithApplication&quot;)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" folderName="App Monolith">
<module name="flexinale-monolith" />
<option name="SPRING_BOOT_MAIN_CLASS" value="de.accso.flexinale.FlexinaleMonolithApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -0,0 +1,25 @@
package de.accso.flexinale;
import jakarta.persistence.EntityManagerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableJpaRepositories("de.accso.flexinale.data.persistence")
@EnableTransactionManagement
public class FlexinaleMonolithApplication {
public static void main(String[] args) {
SpringApplication.run(FlexinaleMonolithApplication.class, args);
}
@Bean
public PlatformTransactionManager transactionManager(final EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}

View file

@ -0,0 +1,65 @@
package de.accso.flexinale.application;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.model.Film;
import de.accso.flexinale.data.model.Vorfuehrung;
import de.accso.flexinale.data.persistence.FilmDao;
import de.accso.flexinale.data.persistence.TicketDao;
import de.accso.flexinale.data.persistence.VorfuehrungDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@SuppressWarnings("unused")
public class FilmService {
@Autowired
private FilmDao filmDao;
@Autowired
private VorfuehrungDao vorfuehrungDao;
@Autowired
private TicketDao ticketDao;
public List<Film> filme() {
return filmDao.findAll();
}
public Film film(String id) {
return filmDao.findById(id).orElse(null);
}
public List<Vorfuehrung> vorfuehrungenFuer(String filmId) {
return vorfuehrungDao.findByFilmId(filmId);
}
public List<String> vorfuehrungenMitUeberlapp(final List<Vorfuehrung> vorfuehrungen, final Benutzer benutzer) {
List<String> vorfuehrungenMitUeberlapp = new ArrayList<>();
TicketBundle ticketBundle = new TicketBundle(benutzer, ticketDao);
for (Vorfuehrung vorfuehrung : vorfuehrungen) {
if (ticketBundle.mindestensEinTicketImTicketBundleUeberlapptMit(vorfuehrung)) {
vorfuehrungenMitUeberlapp.add(vorfuehrung.id);
}
}
return vorfuehrungenMitUeberlapp;
}
public List<String> vorfuehrungenFuerDieDerBenutzerEinTicketHat(final List<Vorfuehrung> vorfuehrungen, final Benutzer benutzer) {
List<String> vorfuehrungenMitTicket = new ArrayList<>();
TicketBundle ticketBundle = new TicketBundle(benutzer, ticketDao);
for (Vorfuehrung vorfuehrung : vorfuehrungen) {
if (ticketBundle.hatSchonTicketFuer(vorfuehrung)) {
vorfuehrungenMitTicket.add(vorfuehrung.id);
}
}
return vorfuehrungenMitTicket;
}
}

View file

@ -0,0 +1,24 @@
package de.accso.flexinale.application;
import de.accso.flexinale.data.model.Kino;
import de.accso.flexinale.data.persistence.KinoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@SuppressWarnings("unused")
public class KinoService {
@Autowired
private KinoDao kinoDao;
public List<Kino> kinos() {
return kinoDao.findAll();
}
public Kino kino(final String id) {
return kinoDao.findById(id).orElse(null);
}
}

View file

@ -0,0 +1,29 @@
package de.accso.flexinale.application;
@SuppressWarnings("unused")
public class KontingentBereitsAusgeschoepftException extends RuntimeException {
//TODO is this exception needed? replace by boolean value in the "Ticketing" interface's method, which is currently void
// if we keep the exception, then perhaps move as inner class to "Kontingent"?
public KontingentBereitsAusgeschoepftException() {
super();
}
public KontingentBereitsAusgeschoepftException(final String message) {
super(message);
}
public KontingentBereitsAusgeschoepftException(final String message, final Throwable cause) {
super(message, cause);
}
public KontingentBereitsAusgeschoepftException(final Throwable cause) {
super(cause);
}
protected KontingentBereitsAusgeschoepftException(final String message, final Throwable cause,
final boolean enableSuppression, final boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -0,0 +1,35 @@
package de.accso.flexinale.application;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.model.Ticket;
import de.accso.flexinale.data.model.Vorfuehrung;
import de.accso.flexinale.data.persistence.TicketDao;
import de.accso.flexinale.util.Config;
import java.util.List;
public class TicketBundle {
private final Benutzer benutzer;
private final TicketDao ticketDao;
private final long minZeitZwischenVorfuehrungenInMinuten = Config.MIN_ZEIT_ZWISCHEN_VORFUEHRUNGEN_IN_MINUTEN;
public TicketBundle(final Benutzer benutzer, final TicketDao ticketDao) {
this.benutzer = benutzer;
this.ticketDao = ticketDao;
}
public boolean mindestensEinTicketImTicketBundleUeberlapptMit(final Vorfuehrung vorfuehrung) {
List<Ticket> tickets = ticketDao.findByBenutzerOrderByZeit(benutzer);
return tickets.stream().anyMatch(ticket ->
ticket.vorfuehrung.ueberlapptMit(vorfuehrung, minZeitZwischenVorfuehrungenInMinuten));
}
public boolean hatSchonTicketFuer(final Vorfuehrung vorfuehrung) {
List<Ticket> tickets = ticketDao.findByBenutzerOrderByZeit(benutzer);
return tickets.stream()
.map(ticket -> ticket.vorfuehrung).toList().contains(vorfuehrung);
}
}

View file

@ -0,0 +1,56 @@
package de.accso.flexinale.application;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.model.Ticket;
import de.accso.flexinale.data.model.Ticket.VerkaufsKanal;
import de.accso.flexinale.data.model.Vorfuehrung;
import de.accso.flexinale.data.persistence.TicketDao;
import de.accso.flexinale.data.persistence.VorfuehrungDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import static de.accso.flexinale.shared_kernel.Identifiable.Id.uuidString;
@Component
@SuppressWarnings("unused")
public class TicketService {
private static final Logger LOGGER = LoggerFactory.getLogger(TicketService.class);
@Autowired
TicketDao ticketDao;
@Autowired
VorfuehrungDao vorfuehrungDao;
public void loeseGutscheineOnlineFuerVorfuehrungEin(final String vorfuehrungId, final Benutzer benutzer, final int anzahlGutscheine) {
loeseGutscheineFuerVorfuehrungEin(vorfuehrungId, benutzer, anzahlGutscheine, VerkaufsKanal.ONLINE);
}
@SuppressWarnings("SameParameterValue")
private void loeseGutscheineFuerVorfuehrungEin(final String vorfuehrungId, final Benutzer benutzer,
final int anzahlGutscheine, final VerkaufsKanal verkaufsKanal) {
Vorfuehrung vorfuehrung = vorfuehrungDao.findById(vorfuehrungId).get(); //TODO check isPresent() first!
vorfuehrung.verkaufeTickets(verkaufsKanal, anzahlGutscheine);
for (int i = 0; i < anzahlGutscheine; i++) {
Ticket ticket = new Ticket(uuidString(), vorfuehrung.film, vorfuehrung, benutzer, verkaufsKanal);
ticketDao.save(ticket);
}
LOGGER.info("%d ticket(s) bought for Vorfuehrung %s, Besucher %s, using Verkaufskanal %s"
.formatted(anzahlGutscheine, vorfuehrung.id, benutzer.login, verkaufsKanal));
}
public int gesamtZahlDerTicketsFuer(final Benutzer loggedInBenutzer) {
return ticketDao.gesamtZahlDerTicketsFuer(loggedInBenutzer);
}
public List<Ticket> findByBesucherOrderByZeit(final Benutzer eingeloggterBesucher) {
return ticketDao.findByBenutzerOrderByZeit(eingeloggterBesucher);
}
}

View file

@ -0,0 +1,24 @@
package de.accso.flexinale.application;
import de.accso.flexinale.data.model.Vorfuehrung;
import de.accso.flexinale.data.persistence.VorfuehrungDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@SuppressWarnings("unused")
public class VorfuehrungService {
@Autowired
private VorfuehrungDao vorfuehrungDao;
public List<Vorfuehrung> vorfuehrungen() {
return vorfuehrungDao.findAll();
}
public Vorfuehrung vorfuehrung(final String id) {
return vorfuehrungDao.findById(id).orElse(null);
}
}

View file

@ -0,0 +1,93 @@
package de.accso.flexinale.data.model;
import de.accso.flexinale.shared_kernel.Identifiable;
import jakarta.persistence.EnumType;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.io.Serializable;
@jakarta.persistence.Entity
public class Benutzer implements Identifiable, Serializable {
public enum Rolle { // Spring Security needs "ROLE_" as default prefix! Do not change.
@SuppressWarnings("unused") ROLE_ADMIN,
ROLE_BESUCHER
}
@jakarta.persistence.Id
public String id; // primary key
@jakarta.persistence.Column(unique = true)
public String login;
@jakarta.persistence.Column(unique = true)
public String emailAdresse;
@jakarta.persistence.Column
public String name;
@jakarta.persistence.Column
public String vorname;
@jakarta.persistence.Column
@jakarta.persistence.Enumerated(EnumType.STRING)
public Rolle rolle;
@jakarta.persistence.Column
private String passwort;
protected Benutzer() {
}
public Benutzer(final String id, final String login, final String emailAdresse, final String name, final String vorname,
final Rolle rolle) {
this.id = id;
this.login = login;
this.emailAdresse = emailAdresse;
this.name = name;
this.vorname = vorname;
this.rolle = rolle;
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
public String getPasswort() {
return passwort;
}
public void encryptAndSetPassword(final String passwort) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
this.passwort = encoder.encode(passwort);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Benutzer that = (Benutzer) o;
return new EqualsBuilder().append(id, that.id).append(login, that.login)
.append(emailAdresse, that.emailAdresse).append(name, that.name)
.append(vorname, that.vorname).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(id).append(login)
.append(emailAdresse).append(name).append(vorname).toHashCode();
}
}

View file

@ -0,0 +1,70 @@
package de.accso.flexinale.data.model;
import de.accso.flexinale.shared_kernel.Identifiable;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
@jakarta.persistence.Entity
public class Film implements Identifiable, Serializable {
@jakarta.persistence.Id
public String id; // primary key
@jakarta.persistence.Column
public String titel;
@jakarta.persistence.Column
public String imdbUrl;
@jakarta.persistence.Column
public Integer dauerInMinuten;
protected Film() {
}
public Film(String id) {
this.id = id;
}
public Film(String id, String titel, String imdbUrl, Integer dauerInMinuten) {
this.id = id;
this.titel = titel;
this.imdbUrl = imdbUrl;
this.dauerInMinuten = dauerInMinuten;
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Film that = (Film) o;
return new EqualsBuilder()
.append(id, that.id)
.append(titel, that.titel)
.append(imdbUrl, that.imdbUrl)
.append(dauerInMinuten, that.dauerInMinuten)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id).append(titel).append(imdbUrl).append(dauerInMinuten).toHashCode();
}
}

View file

@ -0,0 +1,86 @@
package de.accso.flexinale.data.model;
import de.accso.flexinale.shared_kernel.Identifiable;
import jakarta.persistence.CascadeType;
import jakarta.persistence.FetchType;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@jakarta.persistence.Entity
public class Kino implements Identifiable, Serializable {
@jakarta.persistence.Id
public String id; // primary key
@jakarta.persistence.Column
public String name;
@jakarta.persistence.Column
public String adresse;
@jakarta.persistence.Column
public String emailAdresse;
@jakarta.persistence.OneToMany(mappedBy = "kino", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public Set<KinoSaal> kinoSaele = new HashSet<>();
protected Kino() {
}
public Kino(final String id) {
this.id = id;
}
public Kino(final String id, final String name, final String adresse, final String emailAdresse, final Set<KinoSaal> kinoSaele) {
this.id = id;
this.name = name;
this.adresse = adresse;
this.emailAdresse = emailAdresse;
this.kinoSaele = kinoSaele;
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
public void addSaele(final Collection<KinoSaal> saele) {
kinoSaele.addAll(saele);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Kino that = (Kino) o;
return new EqualsBuilder()
.append(id, that.id)
.append(name, that.name)
.append(adresse, that.adresse)
.append(emailAdresse, that.emailAdresse)
.append(kinoSaele.stream().toList(), that.kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id).append(name).append(adresse).append(emailAdresse)
.append(kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals
.toHashCode();
}
}

View file

@ -0,0 +1,80 @@
package de.accso.flexinale.data.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import de.accso.flexinale.shared_kernel.Identifiable;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.io.Serializable;
@jakarta.persistence.Entity
public class KinoSaal implements Identifiable, Serializable {
@jakarta.persistence.Id
public String id; // primary key
@jakarta.persistence.Column
public String name;
@jakarta.persistence.Column
public Integer anzahlPlaetze = 0;
@jakarta.persistence.ManyToOne
@jakarta.persistence.JoinColumn(name = "kino", nullable = false)
@JsonIgnore
public Kino kino; //TODO bidirectional relation K<->KS. We might want to get rid of the bi-directional relationship between Kino<->KinoSaal (everywhere in M/M1/M2/D, in entities, TOs, perhaps leave them in domain classes. This bidirectional stuff made/makes problems everywhere (in equals, hashCode, mapper code, in de/serialization)
// note that de/serialization for TO classes in Distributed does not work correctly, see commented out assert in de.accso.flexinale.backoffice.api_contract.event.EventSerializationTest
protected KinoSaal() {
}
public KinoSaal(final String id) {
this.id = id;
}
public KinoSaal(final String id, final String name, final Integer anzahlPlaetze, final Kino kino) {
this.id = id;
this.name = name;
this.anzahlPlaetze = anzahlPlaetze;
this.kino = kino;
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
@Override
public String toString() {
return "KinoSaal{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", anzahlPlaetze=" + anzahlPlaetze +
(kino != null ? ", kino.id=" + kino.id : ", kino=null") +
'}';
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
KinoSaal that = (KinoSaal) o;
EqualsBuilder builder = new EqualsBuilder().append(id, that.id).append(name, that.name)
.append(anzahlPlaetze, that.anzahlPlaetze);
if (kino != null) {
builder.append(kino.id, that.kino.id); // do not check kino but only its id (otherwise Stackoverflow error)
}
return builder.isEquals();
}
@Override
public int hashCode() {
HashCodeBuilder builder = new HashCodeBuilder(17, 37).append(id)
.append(name).append(anzahlPlaetze);
if (kino != null) {
builder.append(kino.id); // do not check kino but only its id (otherwise Stackoverflow error)
}
return builder.toHashCode();
}
}

View file

@ -0,0 +1,113 @@
package de.accso.flexinale.data.model;
import de.accso.flexinale.application.KontingentBereitsAusgeschoepftException;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalArgumentException;
import de.accso.flexinale.shared_kernel.Identifiable;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
@jakarta.persistence.Entity
public class Kontingent implements Identifiable, Serializable {
public enum KontingentType {
ONLINE,
ZENTRAL,
KINOKASSE
}
@jakarta.persistence.Id
public String id;
@jakarta.persistence.Column
private Integer restKontingentOnline = 0;
@jakarta.persistence.Column
private Integer restKontingentZentral = 0;
@jakarta.persistence.Column
private Integer restKontingentKinokasse = 0;
protected Kontingent() {
}
public Kontingent(final String id, final int gesamtKapazitaetVerkauf, final int quoteOnline) {
this.id = id;
restKontingentOnline = (int) Math.ceil((float)(gesamtKapazitaetVerkauf * quoteOnline)/100);
restKontingentZentral = (int) Math.floor((float)(gesamtKapazitaetVerkauf - restKontingentOnline)/2);
restKontingentKinokasse = gesamtKapazitaetVerkauf - restKontingentZentral - restKontingentOnline;
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
public Integer getRestKontingentOnline() {
return restKontingentOnline;
}
public Integer getRestKontingentZentral() {
return restKontingentZentral;
}
public Integer getRestKontingentKinokasse() {
return restKontingentKinokasse;
}
public void reduziereKontingent(KontingentType kontingentType, int kontingentReduktion) {
if (kontingentReduktion < 0) {
throw new FlexinaleIllegalArgumentException("reduction of Kontingent has to be >=0 but is "
+ kontingentReduktion);
}
switch (kontingentType) {
case ONLINE ->
restKontingentOnline = reduziereDiesesKontingent(restKontingentOnline, kontingentReduktion);
case ZENTRAL ->
restKontingentZentral = reduziereDiesesKontingent(restKontingentZentral, kontingentReduktion);
case KINOKASSE ->
restKontingentKinokasse = reduziereDiesesKontingent(restKontingentKinokasse, kontingentReduktion);
}
}
private int reduziereDiesesKontingent(final int kontingent, final int reduziereUm) {
int reduziertesKontingent = kontingent - reduziereUm;
if (reduziertesKontingent < 0) {
throw new KontingentBereitsAusgeschoepftException();
}
return reduziertesKontingent;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Kontingent that = (Kontingent) o;
return new EqualsBuilder().append(id, that.id)
.append(restKontingentOnline, that.restKontingentOnline)
.append(restKontingentZentral, that.restKontingentZentral)
.append(restKontingentKinokasse, that.restKontingentKinokasse)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id)
.append(restKontingentOnline).append(restKontingentZentral).append(restKontingentKinokasse)
.toHashCode();
}
}

View file

@ -0,0 +1,79 @@
package de.accso.flexinale.data.model;
import de.accso.flexinale.shared_kernel.Identifiable;
import jakarta.persistence.EnumType;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
@jakarta.persistence.Entity
public class Ticket implements Identifiable, Serializable {
public enum VerkaufsKanal {
ONLINE,
ZENTRAL,
KINOKASSE
}
@jakarta.persistence.Id
public String id; // primary key
@jakarta.persistence.ManyToOne
public Film film;
@jakarta.persistence.ManyToOne
public Vorfuehrung vorfuehrung;
@jakarta.persistence.ManyToOne
public Benutzer benutzer;
@jakarta.persistence.Enumerated(EnumType.STRING)
public VerkaufsKanal verkaufsKanal;
protected Ticket() {
}
public Ticket(final String id, final Film film, final Vorfuehrung vorfuehrung, final Benutzer benutzer,
final VerkaufsKanal verkaufsKanal) {
this.id = id;
this.film = film;
this.vorfuehrung = vorfuehrung;
this.benutzer = benutzer;
this.verkaufsKanal = verkaufsKanal;
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Ticket that = (Ticket) o;
return new EqualsBuilder().append(id, that.id).append(film, that.film)
.append(vorfuehrung, that.vorfuehrung).append(benutzer, that.benutzer)
.append(verkaufsKanal, that.verkaufsKanal)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id).append(film).append(vorfuehrung).append(benutzer)
.append(verkaufsKanal)
.toHashCode();
}
}

View file

@ -0,0 +1,120 @@
package de.accso.flexinale.data.model;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalArgumentException;
import de.accso.flexinale.shared_kernel.Identifiable;
import de.accso.flexinale.util.Config;
import jakarta.persistence.CascadeType;
import jakarta.persistence.FetchType;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
import java.time.LocalDateTime;
@jakarta.persistence.Entity
public final class Vorfuehrung implements Identifiable, Serializable { // TODO is only final as otherwise Spotbugs complains, get rid of Exception in constructor!
@jakarta.persistence.Id
public String id; // primary key
@jakarta.persistence.Column(name = "zeit", columnDefinition = "TIMESTAMP")
public LocalDateTime zeit;
@jakarta.persistence.ManyToOne
public Film film;
@jakarta.persistence.ManyToOne
public KinoSaal kinoSaal;
@jakarta.persistence.OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public Kontingent kontingent;
protected Vorfuehrung() {
}
public Vorfuehrung(final String id, final LocalDateTime zeit, final Film film, final KinoSaal kinoSaal) {
this(id, zeit, film, kinoSaal, Config.QUOTE_ONLINE);
}
public Vorfuehrung(final String id, final LocalDateTime zeit, final Film film, final KinoSaal kinoSaal, final int quoteOnline) {
this.id = id;
this.zeit = zeit;
this.film = film;
this.kinoSaal = kinoSaal;
//TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film?
if (kinoSaal == null) {
throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this +
" must not be null");
}
//TODO this validation should not be done here but in KinoSaal itself
if (kinoSaal.anzahlPlaetze == null) {
throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this +
" must not be null");
}
kontingent = new Kontingent("K" + id, kinoSaal.anzahlPlaetze, quoteOnline);
}
@Override
public Id id() {
return Identifiable.Id.of(id);
}
public int getGesamtkapazitaetVerkauf() {
return kinoSaal.anzahlPlaetze;
}
public void verkaufeTickets(final Ticket.VerkaufsKanal verkaufsKanal, final int anzahlTickets) {
switch (verkaufsKanal) {
case ONLINE ->
kontingent.reduziereKontingent(Kontingent.KontingentType.ONLINE, anzahlTickets);
case ZENTRAL ->
kontingent.reduziereKontingent(Kontingent.KontingentType.ZENTRAL, anzahlTickets);
case KINOKASSE ->
kontingent.reduziereKontingent(Kontingent.KontingentType.KINOKASSE, anzahlTickets);
}
}
public boolean ueberlapptMit(final Vorfuehrung other, final long puffer) {
LocalDateTime begin = this.zeit;
LocalDateTime beginOther = other.zeit;
LocalDateTime end = begin.plusMinutes(this.film.dauerInMinuten);
LocalDateTime endOther = beginOther.plusMinutes(other.film.dauerInMinuten);
return ((begin.isBefore(endOther.plusMinutes(puffer)))
&& (end.isAfter(beginOther.minusMinutes(puffer))));
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
@Override
public boolean equals(final Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Vorfuehrung that = (Vorfuehrung) o;
return new EqualsBuilder()
.append(id, that.id).append(zeit, that.zeit)
.append(film, that.film)
.append(kinoSaal, that.kinoSaal)
.append(kontingent, that.kontingent)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(id).append(zeit).append(film).append(kinoSaal)
.append(kontingent)
.toHashCode();
}
}

View file

@ -0,0 +1,20 @@
package de.accso.flexinale.data.persistence;
import org.springframework.data.annotation.Immutable;
import java.util.Collection;
import java.util.Optional;
@Immutable
@SuppressWarnings("unused")
public interface AbstractDao<E> {
E save(E entity);
Collection<E> findAll();
Optional<E> findById(final String id);
void delete(final E entity);
void deleteAll();
}

View file

@ -0,0 +1,26 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.Benutzer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Collection;
import java.util.Optional;
@SuppressWarnings("unused")
public interface BenutzerDao extends JpaRepository<Benutzer, String>, AbstractDao<Benutzer> {
@Override
Optional<Benutzer> findById(final String id);
@Query("SELECT b FROM Benutzer b WHERE b.name = :name")
Collection<Benutzer> findByName(
@Param("name")
String name);
@Query("SELECT b FROM Benutzer b WHERE b.login = :login")
Optional<Benutzer> findByLogin(
@Param("login")
String login);
}

View file

@ -0,0 +1,13 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.Film;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
@SuppressWarnings("unused")
public interface FilmDao extends JpaRepository<Film, String>, AbstractDao<Film> {
@Override
Optional<Film> findById(final String id);
}

View file

@ -0,0 +1,13 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.Kino;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
@SuppressWarnings("unused")
public interface KinoDao extends JpaRepository<Kino, String>, AbstractDao<Kino> {
@Override
Optional<Kino> findById(final String id);
}

View file

@ -0,0 +1,13 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.KinoSaal;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
@SuppressWarnings("unused")
public interface KinoSaalDao extends JpaRepository<KinoSaal, String>, AbstractDao<KinoSaal> {
@Override
Optional<KinoSaal> findById(final String id);
}

View file

@ -0,0 +1,13 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.Kontingent;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
@SuppressWarnings("unused")
public interface KontingentDao extends JpaRepository<Kontingent, String>, AbstractDao<Kontingent> {
@Override
Optional<Kontingent> findById(final String id);
}

View file

@ -0,0 +1,22 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.model.Ticket;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("unused")
public interface TicketDao extends JpaRepository<Ticket, String>, AbstractDao<Ticket> {
@Override
Optional<Ticket> findById(final String id);
@Query("SELECT t FROM Ticket t WHERE t.benutzer = :benutzer ORDER BY t.vorfuehrung.zeit")
List<Ticket> findByBenutzerOrderByZeit(Benutzer benutzer);
@Query("SELECT count(t) FROM Ticket t WHERE t.benutzer = :benutzer")
int gesamtZahlDerTicketsFuer(Benutzer benutzer);
}

View file

@ -0,0 +1,18 @@
package de.accso.flexinale.data.persistence;
import de.accso.flexinale.data.model.Vorfuehrung;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("unused")
public interface VorfuehrungDao extends JpaRepository<Vorfuehrung, String>, AbstractDao<Vorfuehrung> {
@Override
Optional<Vorfuehrung> findById(final String id);
@Query("SELECT v FROM Vorfuehrung v WHERE v.film.id = :filmId ORDER BY v.zeit")
List<Vorfuehrung> findByFilmId(String filmId);
}

View file

@ -0,0 +1,103 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.persistence.AbstractDao;
import de.accso.flexinale.shared_kernel.Identifiable;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashSet;
@SuppressWarnings("unused")
abstract sealed class AbstractExcelDataUploadService<E extends Identifiable> implements ExcelDataUploadService<E>
permits BenutzerUploadService, FilmUploadService, KinoUploadService, KinoSaalUploadService, VorfuehrungUploadService
{
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExcelDataUploadService.class);
@Override
public void beforeLoad(Object... o) {}
@Override
public void afterLoad(Object... o) {}
@Override
public final Collection<E> loadDataFromExcelSheet(final String resourceName) throws IOException {
try (InputStream stream = AbstractExcelDataUploadService.class.getResourceAsStream(resourceName)) {
if (stream == null) {
throw new FileNotFoundException("resource '%s' not found!".formatted(resourceName));
}
return loadDataFromExcelSheet(stream);
}
}
@Override
public final Collection<E> loadDataFromExcelSheet(final InputStream stream) throws IOException {
Collection<E> dataSet = new HashSet<>();
// Excel sheet (the sheet, not the file!) needs to be named exactly like the entity's class name
// (example: "Kino")
String sheetName = getNameOfExcelDataType();
try (Workbook workbook = new XSSFWorkbook(stream)) {
// iterate through each row of first/desired sheet from the workbook
for (Row excelRow : workbook.getSheet(sheetName)) {
String firstCell = excelRow.getCell(0).getStringCellValue();
// ignore comments, starting with # , load all other rows
if (!firstCell.startsWith("#")) {
E data = createDataFromExcelRow(excelRow);
dataSet.add(data);
}
}
}
return dataSet;
}
@Override
public final int loadDataFromExcelSheetAndPersist(
final String resourceName, final PersistMode mode) throws IOException {
try (InputStream stream = AbstractExcelDataUploadService.class.getResourceAsStream(resourceName)) {
if (stream == null) {
throw new IOException("resource '%s' not found!".formatted(resourceName));
}
return loadDataFromExcelSheetAndPersist(stream, mode);
}
}
@Override
public final int loadDataFromExcelSheetAndPersist(
final InputStream stream, final PersistMode mode) throws IOException {
return loadDataFromExcelSheetAndPersistAndReturn(stream, mode).size();
}
@Override
public final Collection<E> loadDataFromExcelSheetAndPersistAndReturn(
final InputStream stream, final PersistMode mode) throws IOException {
Collection<E> dataCollection = loadDataFromExcelSheet(stream);
Collection<E> dataLoaded = new HashSet<>();
AbstractDao<E> dao = getDao();
// in mode "ADD_ONLY" no updates are allowed
for (E dataFromExcelSheet : dataCollection) {
String id = dataFromExcelSheet.id().id();
if (mode == PersistMode.ADD_ONLY && dao.findById(id).isPresent()) {
String sheetName = getNameOfExcelDataType();
LOGGER.warn("%s %s already exists. Will not be persisted because 'ADD' is active".formatted(sheetName, id));
} else {
dao.save(dataFromExcelSheet);
dataLoaded.add(dataFromExcelSheet);
}
}
return dataLoaded; // only return what really has been loaded
}
}

View file

@ -0,0 +1,62 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.persistence.AbstractDao;
import de.accso.flexinale.data.persistence.BenutzerDao;
import jakarta.transaction.Transactional;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Transactional
public non-sealed class BenutzerUploadService extends AbstractExcelDataUploadService<Benutzer> {
private static final Logger LOGGER = LoggerFactory.getLogger(BenutzerUploadService.class);
@Autowired
private BenutzerDao benutzerDao;
@Override
public AbstractDao<Benutzer> getDao() {
return benutzerDao;
}
@Override
public String getNameOfExcelDataType() {
return Benutzer.class.getSimpleName();
}
@Override
public Benutzer createDataFromExcelRow(final Row excelRow) {
String benutzerId = excelRow.getCell(0).getStringCellValue();
// set authorization roles
Cell cellRolle = excelRow.getCell(6);
Benutzer.Rolle rolle;
if (cellRolle == null || cellRolle.getCellType() == CellType.BLANK) {
rolle = null;
}
else {
rolle = Benutzer.Rolle.valueOf(cellRolle.getStringCellValue());
}
Benutzer benutzer = new Benutzer(
benutzerId,
excelRow.getCell(1).getStringCellValue(),
excelRow.getCell(3).getStringCellValue(),
excelRow.getCell(4).getStringCellValue(),
excelRow.getCell(5).getStringCellValue(),
rolle
);
benutzer.encryptAndSetPassword(excelRow.getCell(2).getStringCellValue());
LOGGER.debug("New Benutzer created: " + benutzer);
return benutzer;
}
}

View file

@ -0,0 +1,25 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.persistence.AbstractDao;
import org.apache.poi.ss.usermodel.Row;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
@SuppressWarnings("unused")
public sealed interface ExcelDataUploadService<E> permits AbstractExcelDataUploadService {
Collection<E> loadDataFromExcelSheet(final String resourceName) throws IOException;
Collection<E> loadDataFromExcelSheet(final InputStream stream) throws IOException;
int loadDataFromExcelSheetAndPersist(final String resourceName, PersistMode mode) throws IOException;
int loadDataFromExcelSheetAndPersist(final InputStream stream, PersistMode mode) throws IOException;
Collection<E> loadDataFromExcelSheetAndPersistAndReturn(final InputStream stream, PersistMode mode) throws IOException;
AbstractDao<E> getDao();
String getNameOfExcelDataType();
void beforeLoad(Object... o);
void afterLoad(Object... o);
E createDataFromExcelRow(final Row row);
}

View file

@ -0,0 +1,56 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.model.Film;
import de.accso.flexinale.data.persistence.AbstractDao;
import de.accso.flexinale.data.persistence.FilmDao;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalArgumentException;
import jakarta.transaction.Transactional;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Transactional
public non-sealed class FilmUploadService extends AbstractExcelDataUploadService<Film> {
private static final Logger LOGGER = LoggerFactory.getLogger(FilmUploadService.class);
@Autowired
private FilmDao filmDao;
@Override
public AbstractDao<Film> getDao() {
return filmDao;
}
@Override
public String getNameOfExcelDataType() {
return Film.class.getSimpleName();
}
@Override
public Film createDataFromExcelRow(final Row excelRow) {
String filmId = excelRow.getCell(0).getStringCellValue();
Cell cellDauer = excelRow.getCell(3);
int dauerInMinuten =
switch(cellDauer.getCellType()) {
case NUMERIC -> (int) cellDauer.getNumericCellValue();
case STRING -> Integer.parseInt(cellDauer.getStringCellValue());
default -> throw new FlexinaleIllegalArgumentException("Dauer is not a number");
};
Film film = new Film(
filmId,
excelRow.getCell(1).getStringCellValue(),
excelRow.getCell(2).getStringCellValue(),
dauerInMinuten
);
LOGGER.debug("New Film created " + film);
return film;
}
}

View file

@ -0,0 +1,83 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.model.Kino;
import de.accso.flexinale.data.model.KinoSaal;
import de.accso.flexinale.data.persistence.AbstractDao;
import de.accso.flexinale.data.persistence.KinoSaalDao;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalArgumentException;
import jakarta.transaction.Transactional;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@Service
@Transactional
public non-sealed class KinoSaalUploadService extends AbstractExcelDataUploadService<KinoSaal> {
private static final Logger LOGGER = LoggerFactory.getLogger(KinoSaalUploadService.class);
@Autowired
private KinoSaalDao kinoSaalDao;
@Override
public AbstractDao<KinoSaal> getDao() {
return kinoSaalDao;
}
@Override
public String getNameOfExcelDataType() {
return KinoSaal.class.getSimpleName();
}
@Override
public KinoSaal createDataFromExcelRow(final Row excelRow) {
String kinoSaalId = excelRow.getCell(0).getStringCellValue();
String kinoId = excelRow.getCell(3).getStringCellValue();
Cell cellAnzahlPlaetze = excelRow.getCell(2);
int anzahlPlaetze =
switch(cellAnzahlPlaetze.getCellType()) {
case NUMERIC -> (int) cellAnzahlPlaetze.getNumericCellValue();
case STRING -> Integer.parseInt(cellAnzahlPlaetze.getStringCellValue());
default -> throw new FlexinaleIllegalArgumentException("Anzahl Plaetze is not a number");
};
KinoSaal kinoSaal = new KinoSaal(
kinoSaalId,
excelRow.getCell(1).getStringCellValue(),
anzahlPlaetze,
new Kino(kinoId)
);
LOGGER.debug("New KinoSaal created: " + kinoSaal);
return kinoSaal;
}
public Map<String, Collection<KinoSaal>> mapKinoSaeleToKino(final Collection<KinoSaal> kinoSaele) {
Map<String, Collection<KinoSaal>> saeleUndKinos = new HashMap<>();
for (KinoSaal saal : kinoSaele) {
String key = saal.kino.id;
Collection<KinoSaal> kinoSaeleAlreadyMapped = saeleUndKinos.get(key);
if (kinoSaeleAlreadyMapped == null) {
HashSet<KinoSaal> saele = new HashSet<>();
saele.add(saal);
saeleUndKinos.put(key, saele);
}
else {
kinoSaeleAlreadyMapped.add(saal);
saeleUndKinos.put(key, kinoSaeleAlreadyMapped);
}
}
return saeleUndKinos;
}
}

View file

@ -0,0 +1,79 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.model.Kino;
import de.accso.flexinale.data.model.KinoSaal;
import de.accso.flexinale.data.persistence.AbstractDao;
import de.accso.flexinale.data.persistence.KinoDao;
import de.accso.flexinale.shared_kernel.DeveloperMistakeException;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalStateException;
import jakarta.transaction.Transactional;
import org.apache.poi.ss.usermodel.Row;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Service
@Transactional
public non-sealed class KinoUploadService extends AbstractExcelDataUploadService<Kino> {
private static final Logger LOGGER = LoggerFactory.getLogger(KinoUploadService.class);
@Autowired
private KinoDao kinoDao;
private final Map<String, Collection<KinoSaal>> alleKinosUndIhreSaele = new HashMap<>();
@Override
public AbstractDao<Kino> getDao() {
return kinoDao;
}
@Override
public String getNameOfExcelDataType() {
return Kino.class.getSimpleName();
}
@Override
public void beforeLoad(Object... o) {
if ((o.length != 1) && (!(o[0] instanceof Map))) {
throw new DeveloperMistakeException("wrong type: expecting a " +
"Map<String, Collection<KinoSaal>> of Kino and their KinoSaele");
}
@SuppressWarnings("unchecked")
Map<String, Collection<KinoSaal>> newKinoAndKinoSaele = (Map<String, Collection<KinoSaal>>) o[0];
this.alleKinosUndIhreSaele.putAll(newKinoAndKinoSaele);
}
@Override
public void afterLoad(Object... o) {
alleKinosUndIhreSaele.clear();
}
@Override
public Kino createDataFromExcelRow(final Row excelRow) {
String kinoId = excelRow.getCell(0).getStringCellValue();
Collection<KinoSaal> saeleImKino = alleKinosUndIhreSaele.get(kinoId);
if (saeleImKino == null || saeleImKino.isEmpty()) {
throw new FlexinaleIllegalStateException("no Saele for Kino %s found".formatted(kinoId));
}
Kino kino = new Kino(
kinoId,
excelRow.getCell(1).getStringCellValue(),
excelRow.getCell(2).getStringCellValue(),
excelRow.getCell(3).getStringCellValue(),
Set.copyOf(saeleImKino)
);
LOGGER.debug("New Kino created: " + kino);
return kino;
}
}

View file

@ -0,0 +1,9 @@
package de.accso.flexinale.data.services;
public enum PersistMode {
// updates on existing entities allowed (beware of inconsistencies with derived entities!)
UPDATE,
// only new data can be added, no updates on existing entities allowed
ADD_ONLY
}

View file

@ -0,0 +1,55 @@
package de.accso.flexinale.data.services;
import de.accso.flexinale.data.model.Film;
import de.accso.flexinale.data.model.KinoSaal;
import de.accso.flexinale.data.model.Vorfuehrung;
import de.accso.flexinale.data.persistence.AbstractDao;
import de.accso.flexinale.data.persistence.KinoSaalDao;
import de.accso.flexinale.data.persistence.VorfuehrungDao;
import jakarta.transaction.Transactional;
import org.apache.poi.ss.usermodel.Row;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
@Transactional
public non-sealed class VorfuehrungUploadService extends AbstractExcelDataUploadService<Vorfuehrung> {
private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungUploadService.class);
@Autowired
private KinoSaalDao kinoSaalDao;
@Autowired
private VorfuehrungDao vorfuehrungDao;
@Override
public AbstractDao<Vorfuehrung> getDao() {
return vorfuehrungDao;
}
@Override
public String getNameOfExcelDataType() {
return Vorfuehrung.class.getSimpleName();
}
@Override
public Vorfuehrung createDataFromExcelRow(final Row excelRow) {
String vorfuehrungId = excelRow.getCell(0).getStringCellValue();
// expects date/time in format like for example 2023-02-20T17:45
LocalDateTime zeit = LocalDateTime.parse(excelRow.getCell(1).getStringCellValue());
Film film = new Film(excelRow.getCell(2).getStringCellValue());
String kinoSaalId = excelRow.getCell(3).getStringCellValue();
KinoSaal kinoSaal = kinoSaalDao.findById(kinoSaalId).orElseThrow(IllegalStateException::new);
Vorfuehrung vorfuehrung = new Vorfuehrung(vorfuehrungId, zeit, film, kinoSaal);
LOGGER.debug("New Vorfuehrung created: " + vorfuehrung);
return vorfuehrung;
}
}

View file

@ -0,0 +1,42 @@
package de.accso.flexinale.rest;
import de.accso.flexinale.data.services.ExcelDataUploadService;
import de.accso.flexinale.data.services.PersistMode;
import de.accso.flexinale.shared_kernel.ExcelHelper;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalStateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream;
import java.util.Collection;
abstract class AbstractExcelRestController {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExcelRestController.class);
@SuppressWarnings("rawtypes")
protected RestUploadResult uploadViaRestCall(final ExcelDataUploadService excelDataUploadService, final MultipartFile file) {
try(BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) {
if (!ExcelHelper.isExcelFile(stream)) {
String message = "Please upload a valid Excel file!";
throw new FlexinaleIllegalStateException(message);
}
Collection uploadedData =
excelDataUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.ADD_ONLY);
String message = "Uploaded the Excel file %s adding %d new entities (while not updating any existing data)."
.formatted(file.getOriginalFilename(), uploadedData.size());
return new RestUploadResult(HttpStatus.OK, message);
}
catch (Exception ex) {
String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage());
RestBadRequestException exception = new RestBadRequestException(message, ex);
LOGGER.error(message, exception);
throw exception;
}
}
}

View file

@ -0,0 +1,49 @@
package de.accso.flexinale.rest;
import de.accso.flexinale.application.FilmService;
import de.accso.flexinale.data.services.FilmUploadService;
import de.accso.flexinale.data.model.Film;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@Transactional
public class FilmRestController extends AbstractExcelRestController {
@Autowired
private FilmService filmService;
@Autowired
private FilmUploadService filmUploadService;
@GetMapping(value = "/rest/filme", produces = "application/json")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public List<Film> filme() {
return filmService.filme();
}
@GetMapping(value = "/rest/film/{id}", produces = "application/json")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public Film film(@PathVariable final String id) {
Film film = filmService.film(id);
if (film == null) {
throw new RestResourceNotFoundException("no Film %s found.".formatted(id));
}
return film;
}
@PostMapping("/rest/filme")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<RestResponseMessage> uploadFilme(@RequestParam("file") final MultipartFile file) {
RestUploadResult restUploadResult = uploadViaRestCall(filmUploadService, file);
return ResponseEntity.status(restUploadResult.status())
.body(new RestResponseMessage(restUploadResult.message()));
}
}

View file

@ -0,0 +1,78 @@
package de.accso.flexinale.rest;
import de.accso.flexinale.application.KinoService;
import de.accso.flexinale.data.services.KinoUploadService;
import de.accso.flexinale.data.services.KinoSaalUploadService;
import de.accso.flexinale.data.model.Kino;
import de.accso.flexinale.data.model.KinoSaal;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@RestController
@Transactional
public class KinoKinoSaalRestController extends AbstractExcelRestController {
@Autowired
private KinoService kinoService;
@Autowired
private KinoUploadService kinoUploadService;
@Autowired
private KinoSaalUploadService kinoSaalUploadService;
@GetMapping(value = "/rest/kinos", produces = "application/json")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public List<Kino> kinos() {
return kinoService.kinos();
}
@GetMapping(value = "/rest/kino/{id}", produces = "application/json")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public Kino kino(@PathVariable final String id) {
Kino kino = kinoService.kino(id);
if (kino == null) {
throw new RestResourceNotFoundException("no Kino %s found.".formatted(id));
}
return kino;
}
@PostMapping("/rest/kinos")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<RestResponseMessage> uploadKinosKinoSaele(@RequestParam("file") final MultipartFile file) {
try(BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) {
// load KinoSaele first
Collection<KinoSaal> alleKinoSaele =
kinoSaalUploadService.loadDataFromExcelSheet(stream);
Map<String, Collection<KinoSaal>> alleKinosUndIhreSaele =
kinoSaalUploadService.mapKinoSaeleToKino(alleKinoSaele);
// .. then persist them as part of a Kino
RestUploadResult restUploadResult;
try {
kinoUploadService.beforeLoad(alleKinosUndIhreSaele); // fix state of Kino-KinoSaal in map
restUploadResult = uploadViaRestCall(kinoUploadService, file);
}
finally {
kinoUploadService.afterLoad(); // clear map
}
return ResponseEntity.status(restUploadResult.status())
.body(new RestResponseMessage(restUploadResult.message()));
}
catch (Exception ex) {
String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage());
throw new RestBadRequestException(message, ex);
}
}
}

View file

@ -0,0 +1,19 @@
package de.accso.flexinale.rest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
@SuppressWarnings("unused")
public class RestBadRequestException extends RuntimeException {
// see https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services
public RestBadRequestException(final String message) {
super(message);
}
public RestBadRequestException(final String message, final Exception ex) {
super(message, ex);
}
}

View file

@ -0,0 +1,14 @@
package de.accso.flexinale.rest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class RestResourceNotFoundException extends RuntimeException {
// see https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services
public RestResourceNotFoundException(final String message) {
super(message);
}
}

View file

@ -0,0 +1,4 @@
package de.accso.flexinale.rest;
record RestResponseMessage(String message) {
}

View file

@ -0,0 +1,7 @@
package de.accso.flexinale.rest;
import org.springframework.http.HttpStatus;
@SuppressWarnings("SameParameterValue")
record RestUploadResult(HttpStatus status, String message) {
}

View file

@ -0,0 +1,49 @@
package de.accso.flexinale.rest;
import de.accso.flexinale.application.VorfuehrungService;
import de.accso.flexinale.data.services.VorfuehrungUploadService;
import de.accso.flexinale.data.model.Vorfuehrung;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@Transactional
public class VorfuehrungRestController extends AbstractExcelRestController {
@Autowired
private VorfuehrungService vorfuehrungService;
@Autowired
private VorfuehrungUploadService vorfuehrungUploadService;
@GetMapping(value = "/rest/vorfuehrungen", produces = "application/json")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public List<Vorfuehrung> vorfuehrungen() {
return vorfuehrungService.vorfuehrungen();
}
@GetMapping(value = "/rest/vorfuehrungen/{id}", produces = "application/json")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public Vorfuehrung vorfuehrung(@PathVariable final String id) {
Vorfuehrung vorfuehrung = vorfuehrungService.vorfuehrung(id);
if (vorfuehrung == null) {
throw new RestResourceNotFoundException("no Vorfuehrung %s found.".formatted(id));
}
return vorfuehrung;
}
@PostMapping("/rest/vorfuehrungen")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<RestResponseMessage> uploadVorfuehrungen(@RequestParam("file") final MultipartFile file) {
RestUploadResult restUploadResult = uploadViaRestCall(vorfuehrungUploadService, file);
return ResponseEntity.status(restUploadResult.status())
.body(new RestResponseMessage(restUploadResult.message()));
}
}

View file

@ -0,0 +1,59 @@
package de.accso.flexinale.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ApplicationSecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
@Bean
public AuthenticationManager authManager(final HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(authenticationProvider());
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
// see here: https://www.baeldung.com/spring-security-csrf
// otherwise, the rest POST requests fail with 401 errors
http.csrf(c -> c.ignoringRequestMatchers("/rest/**"));
return http.build();
}
}

View file

@ -0,0 +1,45 @@
package de.accso.flexinale.security;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.persistence.BenutzerDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@Service
public class BenutzerDetailsService implements UserDetailsService {
@Autowired
private BenutzerDao benutzerDao;
@Override
public UserDetails loadUserByUsername(final String login) throws UsernameNotFoundException {
Optional<Benutzer> benutzer = benutzerDao.findByLogin(login);
if (benutzer.isEmpty()) { // login is unique
throw new UsernameNotFoundException("no Benutzer %s found".formatted(login));
}
return new UserPrincipal(benutzer.get());
}
public Benutzer getLoggedInBenutzer() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userLogin = authentication.getName();
Optional<Benutzer> benutzer = benutzerDao.findByLogin(userLogin);
if (benutzer.isEmpty()) {
throw new ResponseStatusException(HttpStatus.EXPECTATION_FAILED,
"None or more than one Besucher with login " + userLogin);
}
else {
return benutzer.get();
}
}
}

View file

@ -0,0 +1,8 @@
package de.accso.flexinale.security;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

View file

@ -0,0 +1,43 @@
package de.accso.flexinale.security;
import de.accso.flexinale.data.model.Benutzer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class UserPrincipal implements UserDetails {
private final Benutzer benutzer;
public UserPrincipal(Benutzer user) {
this.benutzer = user;
}
@Override
@SuppressWarnings("Convert2Lambda")
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority authority = new GrantedAuthority() {
@Override
public String getAuthority() {
return benutzer.rolle.toString();
}
};
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return benutzer.getPasswort();
}
@Override
public String getUsername() {
return benutzer.login;
}
}

View file

@ -0,0 +1,24 @@
package de.accso.flexinale.shared_kernel;
public class DeveloperMistakeException extends RuntimeException {
public DeveloperMistakeException(final String message) {
super(message);
}
public DeveloperMistakeException() {
super();
}
public DeveloperMistakeException(final String message, final Throwable cause) {
super(message, cause);
}
public DeveloperMistakeException(final Throwable cause) {
super(cause);
}
protected DeveloperMistakeException(final String message, final Throwable cause,
final boolean enableSuppression, final boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -0,0 +1,15 @@
package de.accso.flexinale.shared_kernel;
import org.apache.poi.poifs.filesystem.FileMagic;
import java.io.IOException;
import java.io.InputStream;
public final class ExcelHelper {
private ExcelHelper() {
}
public static boolean isExcelFile(final InputStream stream) throws IOException {
return (FileMagic.valueOf(stream) == FileMagic.OOXML);
}
}

View file

@ -0,0 +1,25 @@
package de.accso.flexinale.shared_kernel;
@SuppressWarnings("unused")
public class FlexinaleIllegalArgumentException extends DeveloperMistakeException {
public FlexinaleIllegalArgumentException() {
super();
}
public FlexinaleIllegalArgumentException(final String message) {
super(message);
}
public FlexinaleIllegalArgumentException(final String message, final Throwable cause) {
super(message, cause);
}
public FlexinaleIllegalArgumentException(final Throwable cause) {
super(cause);
}
protected FlexinaleIllegalArgumentException(final String message, final Throwable cause,
final boolean enableSuppression, final boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -0,0 +1,20 @@
package de.accso.flexinale.shared_kernel;
@SuppressWarnings("unused")
public class FlexinaleIllegalStateException extends IllegalStateException {
public FlexinaleIllegalStateException() {
super();
}
public FlexinaleIllegalStateException(final String message) {
super(message);
}
public FlexinaleIllegalStateException(final String message, final Throwable cause) {
super(message, cause);
}
public FlexinaleIllegalStateException(final Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,26 @@
package de.accso.flexinale.shared_kernel;
import java.io.Serializable;
import java.util.UUID;
public interface Identifiable {
record Id(String id) implements Serializable {
public static Id of() {
return uuid();
}
public static Id of(String id) {
return new Id(id);
}
public static Id uuid() {
return Id.of(UUID.randomUUID().toString());
}
public static String uuidString() {
return UUID.randomUUID().toString();
}
}
Id id();
}

View file

@ -0,0 +1,14 @@
package de.accso.flexinale.shared_kernel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TimeFormatter {
public static String calculateString(final LocalDateTime from, final Integer durationInMinutes) {
DateTimeFormatter hourMinuteFormatter = DateTimeFormatter.ofPattern("HH:mm");
return hourMinuteFormatter.format(from)
+ " - "
+ hourMinuteFormatter.format(from.plusMinutes(durationInMinutes));
}
}

View file

@ -0,0 +1,51 @@
package de.accso.flexinale.util;
import de.accso.flexinale.shared_kernel.FlexinaleIllegalArgumentException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
@Value("${application.title}")
public String applicationTitle;
@Value("${build.version}")
public String buildVersion;
@Value("${build.date}")
public String buildDate;
// -----------------------------------------------------------------------------------------------------------
// percentage
public static int QUOTE_ONLINE;
@Value("${de.accso.flexinale.kontingent.quote.online:33}")
private int quoteOnline;
@Value("${de.accso.flexinale.kontingent.quote.online:33}")
public void setQuotierung(final int quoteOnline) {
if (quoteOnline < 0 || quoteOnline > 100) {
String message = "Quote online should be a percentage, i.e. 0 <= quote online <= 100, but was " + quoteOnline;
throw new FlexinaleIllegalArgumentException(message);
}
Config.QUOTE_ONLINE = quoteOnline;
}
// -----------------------------------------------------------------------------------------------------------
// time in minutes
public static int MIN_ZEIT_ZWISCHEN_VORFUEHRUNGEN_IN_MINUTEN;
@Value("${de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten:30}")
private int minZeitZwischenVorfuehrungenInMinuten;
@Value("${de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten:30}")
public void setMinZeitZwischenVorfuehrungenInMinuten(final int minZeitZwischenVorfuehrungenInMinuten) {
if (minZeitZwischenVorfuehrungenInMinuten < 0 ) {
String message = "minZeitZwischenVorfuehrungenInMinuten should be positiv, but was " + minZeitZwischenVorfuehrungenInMinuten;
throw new FlexinaleIllegalArgumentException(message);
}
Config.MIN_ZEIT_ZWISCHEN_VORFUEHRUNGEN_IN_MINUTEN = minZeitZwischenVorfuehrungenInMinuten;
}
}

View file

@ -0,0 +1,64 @@
package de.accso.flexinale.web;
import de.accso.flexinale.application.FilmService;
import de.accso.flexinale.data.model.Benutzer;
import de.accso.flexinale.data.model.Film;
import de.accso.flexinale.data.model.Vorfuehrung;
import de.accso.flexinale.security.BenutzerDetailsService;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
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
@Transactional
public class FilmWebController {
@Autowired
private FilmService filmService;
@Autowired
private BenutzerDetailsService benutzerService;
@GetMapping(value="/filme")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
@SuppressWarnings("SameReturnValue")
public String filme(final Model model) {
List<Film> 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) {
Film film = filmService.film(id);
if (film == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "film %s not available".formatted(id));
}
else {
model.addAttribute("film", film);
List<Vorfuehrung> vorfuehrungenOfFilm = filmService.vorfuehrungenFuer(id);
model.addAttribute("vorfuehrungen", vorfuehrungenOfFilm);
if (!vorfuehrungenOfFilm.isEmpty()) {
Benutzer benutzerLoggedIn = benutzerService.getLoggedInBenutzer();
List<String> vorfuehrungenMitUeberlapp =
filmService.vorfuehrungenMitUeberlapp(vorfuehrungenOfFilm, benutzerLoggedIn);
model.addAttribute("vorfuehrungenMitUeberlapp", vorfuehrungenMitUeberlapp);
List<String> vorfuehrungenMitTicket =
filmService.vorfuehrungenFuerDieDerBenutzerEinTicketHat(vorfuehrungenOfFilm, benutzerLoggedIn);
model.addAttribute("vorfuehrungenMitTicket", vorfuehrungenMitTicket);
}
}
return "film";
}
}

View file

@ -0,0 +1,23 @@
package de.accso.flexinale.web;
import de.accso.flexinale.util.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexWebController {
@Autowired
private Config config;
@GetMapping(value="/")
@SuppressWarnings("SameReturnValue")
public String index(final Model model) {
model.addAttribute("applicationTitle", config.applicationTitle);
model.addAttribute("buildVersion", config.buildVersion);
model.addAttribute("buildDate", config.buildDate);
return "index";
}
}

View file

@ -0,0 +1,46 @@
package de.accso.flexinale.web;
import de.accso.flexinale.application.KinoService;
import de.accso.flexinale.data.model.Kino;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
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
@Transactional
public class KinoWebController {
@Autowired
private KinoService kinoService;
@GetMapping(value="/kinos")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
@SuppressWarnings("SameReturnValue")
public String kinos(final Model model) {
List<Kino> 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) {
Kino kino = kinoService.kino(id);
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,84 @@
package de.accso.flexinale.web;
import de.accso.flexinale.data.model.Ticket;
import de.accso.flexinale.data.model.Vorfuehrung;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
class TicketSorter {
/**
* return a list of list of tickets, sorted by time (inner list) and date (outer list).
* each list contains the tickets for a day
*/
static List<List<VorfuehrungMitAnzahlTicketsTO>> sortAndMapTicketsPerDay(final List<Ticket> allTickets) {
if (allTickets == null || allTickets.isEmpty()) {
return new ArrayList<>();
}
// 1) tickets by day
List<List<Ticket>> ticketsProTag = new ArrayList<>();
LocalDate previousDate = LocalDate.MIN; // far past :-)
for (Ticket ticket : allTickets) {
LocalDate currentDate = ticket.vorfuehrung.zeit.toLocalDate();
if (currentDate.equals(previousDate)) {
ticketsProTag.getLast().add(ticket);
}
else {
List<Ticket> 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<Ticket> ticketsForOneDay : ticketsProTag) {
List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenUndAnzahlTicketsFuerEinenTag = new ArrayList<>();
int anzahlTicketsFuerVorfuehrung = 0;
Vorfuehrung previousVorfuehrung = ticketsForOneDay.getFirst().vorfuehrung;
for (Ticket 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.
Vorfuehrung currentVorfuehrung = ticket.vorfuehrung;
if (currentVorfuehrung.equals(previousVorfuehrung)) {
anzahlTicketsFuerVorfuehrung++;
}
else {
// add the information for previous Vorfuehrung
VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
anzahlTicketsFuerVorfuehrung, previousVorfuehrung);
vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
anzahlTicketsFuerVorfuehrung = 1; // ... we also have a new ticket for the next Vorfuehrung
}
previousVorfuehrung = currentVorfuehrung;
}
// Also add the last Vorfuehrung an anzahlTickets when day is over
VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
anzahlTicketsFuerVorfuehrung, previousVorfuehrung);
vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag.add(vorfuehrungenUndAnzahlTicketsFuerEinenTag);
}
return vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag;
}
private static VorfuehrungMitAnzahlTicketsTO mapVorfuehrungToTO(final int anzahlTicketsFuerVorfuehrung,
final Vorfuehrung previousVorfuehrung) {
return new VorfuehrungMitAnzahlTicketsTO(
previousVorfuehrung.zeit, previousVorfuehrung.film,
previousVorfuehrung.kinoSaal,
anzahlTicketsFuerVorfuehrung
);
}
}

View file

@ -0,0 +1,82 @@
package de.accso.flexinale.web;
import de.accso.flexinale.application.KontingentBereitsAusgeschoepftException;
import de.accso.flexinale.application.TicketService;
import de.accso.flexinale.application.VorfuehrungService;
import de.accso.flexinale.data.model.*;
import de.accso.flexinale.security.BenutzerDetailsService;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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
@Transactional
public class TicketWebController {
private static final Logger LOGGER = LoggerFactory.getLogger(TicketWebController.class);
@Autowired
private TicketService ticketService;
@Autowired
private VorfuehrungService vorfuehrungService;
@Autowired
private BenutzerDetailsService benutzerService;
@GetMapping(value="/tickets")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public String listTickets(final Model model) {
Benutzer loggedInBenutzer = benutzerService.getLoggedInBenutzer();
List<Ticket> allTickets = ticketService.findByBesucherOrderByZeit(loggedInBenutzer);
List<List<VorfuehrungMitAnzahlTicketsTO>> ticketsByDay = TicketSorter.sortAndMapTicketsPerDay(allTickets);
model.addAttribute("ticketsByDay", ticketsByDay);
int totalNumberOfTickets = ticketService.gesamtZahlDerTicketsFuer(loggedInBenutzer);
model.addAttribute("totalNumberOfTickets", totalNumberOfTickets);
return ("tickets");
}
@PostMapping("/vorfuehrung/loeseGutscheineOnlineEin")
@PreAuthorize("hasRole('ROLE_BESUCHER')")
public String loeseGutscheineOnlineEin(
@RequestParam("vorfuehrungId") final String vorfuehrungId,
@RequestParam("anzahl") final int anzahl,
final RedirectAttributes redirAttrs) {
Benutzer loggedInBenutzer = benutzerService.getLoggedInBenutzer();
try {
ticketService.loeseGutscheineOnlineFuerVorfuehrungEin(vorfuehrungId, loggedInBenutzer, anzahl);
}
catch (KontingentBereitsAusgeschoepftException kbaEx) {
String message = "Kontingent exceeded: no %d tickets for Vorfuehrung %s and Besucher %s available"
.formatted(anzahl, vorfuehrungId, loggedInBenutzer.login);
LOGGER.info(message);
Vorfuehrung vorfuehrung = vorfuehrungService.vorfuehrung(vorfuehrungId);
String filmId = vorfuehrung.film.id;
redirAttrs.addFlashAttribute("error", "Keine %d Tickets mehr vorhanden".formatted(anzahl));
return "redirect:/film/" + filmId;
}
redirAttrs.addFlashAttribute("success", "%d Ticket(s) erfolgreich gekauft".formatted(anzahl));
return "redirect:/tickets";
}
}
// class used to "flatten" Vorfuehrung and number of Tickets for Vorfuehrung for Clients
record VorfuehrungMitAnzahlTicketsTO(LocalDateTime zeit, Film film, KinoSaal kinoSaal, int anzahlTickets) { }

View file

@ -0,0 +1,53 @@
application.title=FLEXinale Monolith
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/monolith
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
#########################################################################
# 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,9 @@
-------------------------------------------------------------------------
,------. ,--. ,------. ,--. ,--. ,--. ,--.
| .---' | | | .---' \ `.' / `--' ,--,--, ,--,--. | | ,---.
| `--, | | | `--, .' \ ,--. | \ ' ,-. | | | | .-. :
| |` | '--. | `---. / .'. \ | | | || | \ '-' | | | \ --.
`--' `-----' `------' '--' '--' `--' `--''--' `--`--' `--' `----'
${application.title} (version ${application.version}) on port ${server.port}
Powered by Spring Boot ${spring-boot.version}
-------------------------------------------------------------------------

View file

@ -0,0 +1,11 @@
<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="de.accso" level="INFO"/>
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

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,90 @@
.header {
margin: 15px;
}
.nav.black {
background-color: #000000;
}
.FLEXnav.monolith {
background-color: lightcoral;
}
.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;
}
.FLEXerrorSmall {
margin: 15px;
font-size: 200%;
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;
}

File diff suppressed because one or more lines are too long

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; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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);
});
});
}());
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

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;
}

Some files were not shown because too many files have changed in this diff Show more