[P25284-20] Initial commit with setup and first draft rest servcice

This commit is contained in:
verboomp
2026-01-20 11:39:25 +01:00
parent 071b1f7f95
commit 9d36ea7780
66 changed files with 4981 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
package marketing.heyday.hartmann.fotodocumentation.core.db.migration;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.TransactionManagement;
import jakarta.ejb.TransactionManagementType;
import javax.sql.DataSource;
import org.flywaydb.core.Flyway;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Singleton
@Startup
@TransactionManagement(TransactionManagementType.BEAN)
public class DbMigrator {
@Resource(lookup = "java:/jdbc/fotoDocumentationDS")
private DataSource dataSource;
@PostConstruct
public void onStartup() {
Flyway flyway = getFlyway();
flyway.setDataSource(dataSource);
flyway.setTable("DB_MIGRATION");
flyway.setLocations(this.getClass().getPackage().getName());
flyway.setBaselineOnMigrate(false);
flyway.setValidateOnMigrate(false);
flyway.setIgnoreFailedFutureMigration(true);
flyway.migrate();
}
protected Flyway getFlyway() {
return new Flyway();
}
}

View File

@@ -0,0 +1,71 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import jakarta.persistence.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@MappedSuperclass
public abstract class AbstractDateEntity extends AbstractEntity {
private static final long serialVersionUID = 1L;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "JPA_CREATED", nullable = false)
@JsonIgnore
private Date jpaCreated;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "JPA_UPDATED", nullable = false)
@JsonIgnore
private Date jpaUpdated;
@Column(name = "JPA_ACTIVE", nullable = false)
private boolean active = true;
@Version
@Column(name = "JPA_VERSION", nullable = false)
private int jpaVersion = 0;
@PrePersist
protected void onCreate() {
Date now = new Date();
jpaCreated = now;
jpaUpdated = now;
}
@PreUpdate
protected void onUpdate() {
jpaUpdated = new Date();
}
public Date getJpaCreated() {
return jpaCreated;
}
public Date getJpaUpdated() {
return jpaUpdated;
}
public int getJpaVersion() {
return jpaVersion;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}

View File

@@ -0,0 +1,21 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.io.Serializable;
import jakarta.persistence.MappedSuperclass;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,108 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "customer")
@NamedQuery(name = Customer.FIND_BY_NUMBER, query = "select c from Customer c where c.customerNumber = :cutomerNumber")
public class Customer extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "customer_seq";
public static final String FIND_BY_NUMBER = "Customer.findByNumber";
public static final String PARAM_NUMBER = "cutomerNumber";
@Id
@Column(name = "customer_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long customerId;
@Column(name = "customer_number", unique = true, nullable = false)
private String customerNumber;
@Column(name = "name", nullable = false)
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "customer_id_fk")
private Set<Picture> pictures = new HashSet<>();
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerNumber() {
return customerNumber;
}
public void setCustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Picture> getPictures() {
return pictures;
}
public void setPictures(Set<Picture> pictures) {
this.pictures = pictures;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(customerNumber).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.customerNumber.equals(((Customer) obj).getCustomerNumber());
}
public static class Builder {
private Customer instance = new Customer();
public Builder customerNumber(String customerNumber) {
instance.setCustomerNumber(customerNumber);
return this;
}
public Builder name(String name) {
instance.setName(name);
return this;
}
public Customer build() {
return instance;
}
}
}

View File

@@ -0,0 +1,125 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.Date;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "picture")
public class Picture extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "picture_seq";
@Id
@Column(name = "picture_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long pictureId;
// username from the person that shot the picture
@Column(name = "username")
private String username;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "picture_date", nullable = false)
private Date pictureDate;
private String comment;
@Column(name = "image")
private String image;
public Long getPictureId() {
return pictureId;
}
public void setPictureId(Long pictureId) {
this.pictureId = pictureId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getPictureDate() {
return pictureDate;
}
public void setPictureDate(Date pictureDate) {
this.pictureDate = pictureDate;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(pictureId).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass() || pictureId == null) {
return false;
}
return this.pictureId.equals(((Picture) obj).getPictureId());
}
public static class Builder {
private Picture instance = new Picture();
public Builder username(String username) {
instance.setUsername(username);
return this;
}
public Builder pictureDate(Date pictureDate) {
instance.setPictureDate(pictureDate);
return this;
}
public Builder comment(String comment) {
instance.setComment(comment);
return this;
}
public Builder image(String image) {
instance.setImage(image);
return this;
}
public Picture build() {
return instance;
}
}
}

View File

@@ -0,0 +1,95 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "x_right")
public class Right extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "right_seq";
@Id
@Column(name = "right_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long rightId;
@Column(nullable = false, unique = true)
private String code;
@Column(nullable = false, unique = true)
private String name;
@ManyToMany(mappedBy = "rights")
private Set<User> roles = new HashSet<>();
public Long getRightId() {
return rightId;
}
public void setRightId(Long rightId) {
this.rightId = rightId;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(code).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.code.equals(((Right) obj).getCode());
}
public static class Builder {
private Right instance = new Right();
public Builder code(String code) {
instance.setCode(code);
return this;
}
public Builder name(String name) {
instance.setName(name);
return this;
}
public Right build() {
return instance;
}
}
}

View File

@@ -0,0 +1,179 @@
package marketing.heyday.hartmann.fotodocumentation.core.model;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang.builder.HashCodeBuilder;
import jakarta.persistence.*;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Entity
@Table(name = "x_user")
public class User extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "user_seq";
@Id
@Column(name = "user_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
@SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1)
private Long userId;
@Column(name = "username", unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String salt;
@Column(name = "title")
private String title;
@Column(nullable = false)
private String firstname;
@Column(nullable = false)
private String lastname;
@Column(nullable = false)
private String email;
@ManyToMany
@JoinTable(name = "user_to_right", joinColumns = { @JoinColumn(name = "user_id_fk") }, inverseJoinColumns = { @JoinColumn(name = "right_id_fk") })
private Set<Right> rights = new HashSet<>();
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Set<Right> getRights() {
return rights;
}
public void setRights(Set<Right> rights) {
this.rights = rights;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(username).toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
return this.username.equals(((User) obj).getUsername());
}
public static class Builder {
private User instance = new User();
public Builder email(String email) {
instance.setEmail(email);
return this;
}
public Builder firstname(String firstname) {
instance.setFirstname(firstname);
return this;
}
public Builder lastname(String lastname) {
instance.setLastname(lastname);
return this;
}
public Builder title(String title) {
instance.setTitle(title);
return this;
}
public Builder username(String username) {
instance.setUsername(username);
return this;
}
public Builder password(String password) {
instance.setPassword(password);
return this;
}
public User build() {
return instance;
}
}
}

View File

@@ -0,0 +1,16 @@
package marketing.heyday.hartmann.fotodocumentation.core.query;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
public record Param(String name, Object value) {
}

View File

@@ -0,0 +1,100 @@
package marketing.heyday.hartmann.fotodocumentation.core.query;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Stateless
@SuppressWarnings("unchecked")
@PermitAll
public class QueryService {
private static final Log LOG = LogFactory.getLog(QueryService.class);
@PersistenceContext
private EntityManager eManager;
public int count(String namedQuery, Param... params) {
Query query = eManager.createNamedQuery(namedQuery);
for (Param param : params) {
query.setParameter(param.name(), param.value());
}
return ((Number) query.getSingleResult()).intValue();
}
public <T> Optional<T> callNamedQuerySingleResult(String namedQuery, Param... params) {
return singleResult(eManager.createNamedQuery(namedQuery), Arrays.asList(params));
}
private <T> Optional<T> singleResult(Query query, Collection<Param> params) {
try {
for (Param param : params) {
query.setParameter(param.name(), param.value());
}
return Optional.ofNullable((T) query.getSingleResult());
} catch (NoResultException nre) {
LOG.debug("No entity found for query " + query + " with params " + params);
LOG.trace("NoResultException", nre);
return Optional.empty();
}
}
public <T> T callNamedQueryList(String namedQuery, Param... objects) {
Query query = eManager.createNamedQuery(namedQuery);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return (T) query.getResultList();
}
public <T> Optional<T> callQuerySingleResult(String sql, Param... objects) {
Query query = eManager.createQuery(sql);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return Optional.of((T) query.getSingleResult());
}
public <T> T callNativeQuery(String sql, Param... objects) {
Query query = eManager.createNativeQuery(sql);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return (T) query.getResultList();
}
public int callNativeQueryUpdate(String sql, Param... objects) {
Query query = eManager.createNativeQuery(sql);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return query.executeUpdate();
}
public int callNamedQueryUpdate(String namedQuery, Param... objects) {
Query query = eManager.createNamedQuery(namedQuery);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return query.executeUpdate();
}
}

View File

@@ -0,0 +1,48 @@
package marketing.heyday.hartmann.fotodocumentation.core.service;
import java.util.Optional;
import jakarta.ejb.EJB;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
import marketing.heyday.hartmann.fotodocumentation.core.query.Param;
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@Stateless
@LocalBean
public class CustomerPictureService {
@PersistenceContext
private EntityManager entityManager;
@EJB
private QueryService queryService;
public boolean addCustomerPicture(CustomerPictureValue customerPictureValue) {
Optional<Customer> customerOpt = queryService.callNamedQuerySingleResult(Customer.FIND_BY_NUMBER, new Param(Customer.PARAM_NUMBER, customerPictureValue.customerNumber()));
Customer customer = customerOpt.orElseGet(() -> new Customer.Builder().customerNumber(customerPictureValue.customerNumber()).name(customerPictureValue.pharmacyName()).build());
Picture picture = new Picture.Builder().username(customerPictureValue.username()).comment(customerPictureValue.comment()).image(customerPictureValue.base64String()).pictureDate(customerPictureValue.date()).build();
customer.getPictures().add(picture);
entityManager.persist(picture);
entityManager.merge(customer);
entityManager.flush();
return true;
}
}

View File

@@ -0,0 +1,49 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import org.jboss.resteasy.annotations.GZIP;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
import marketing.heyday.hartmann.fotodocumentation.rest.jackson.JsonSchemaValidate;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
@RequestScoped
@Path("customer-picture")
public class CustomerPictureResource {
@EJB
private CustomerPictureService customerPictureService;
@GZIP
@POST
@Path("")
//@Authenticate(shouldBeInAllRoles = RightUtils.ADMIN_RIGHT)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Add Customer Image to database")
@ApiResponse(responseCode = "200", description = "Add successfull")
public Response doAddCustomerPicture(@JsonSchemaValidate("schema/customer_picture_add.json") CustomerPictureValue customerPictureValue) {
boolean success = customerPictureService.addCustomerPicture(customerPictureValue);
return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
}
}

View File

@@ -0,0 +1,36 @@
package marketing.heyday.hartmann.fotodocumentation.rest;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 14 Nov 2024
*/
@Path("monitoring")
@RequestScoped
public class MonitoringResource {
@GET
@Path("check/{text}")
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Monitoring service for testing if the server is up and running.")
@ApiResponse(responseCode = "200", description = "ok with as body the given path param text. ")
public Response check(@PathParam("text") String text) {
return Response.status(Status.OK).entity(text).build();
}
}

View File

@@ -0,0 +1,51 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.servers.Server;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.MediaType;
import marketing.heyday.hartmann.fotodocumentation.rest.CustomerPictureResource;
import marketing.heyday.hartmann.fotodocumentation.rest.MonitoringResource;
/**
*
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 18 Oct 2017
*/
@OpenAPIDefinition(info = @Info(title = "Hartmann Photo upload API", version = "1.0", description = "All available routes for the Hartmann Photo upload API", contact = @Contact(url = "https://localhost", name = "Patrick Verboom", email = "p.verboom@heyday.marketing")), servers = {
@Server(description = "development", url = "http://localhost"),
@Server(description = "integration", url = "http://localhost"),
@Server(description = "production", url = "http://localhost")
})
@ApplicationPath("/api")
public class ApplicationConfigApi extends Application {
private static final Log LOG = LogFactory.getLog(ApplicationConfigApi.class);
public static final String JSON_OUT = MediaType.APPLICATION_JSON + "; charset=utf-8";
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> retVal = new HashSet<>();
retVal.add(OpenApiResource.class);
retVal.add(ValidatedMessageBodyReader.class);
//retVal.add(AuthenticateFilter.class);
retVal.add(MonitoringResource.class);
retVal.add(CustomerPictureResource.class);
LOG.info("returning rest api classes " + retVal);
return retVal;
}
}

View File

@@ -0,0 +1,21 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Feb 10, 2017
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.PARAMETER})
public @interface JsonSchemaValidate {
String value();
}

View File

@@ -0,0 +1,14 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
/**
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Feb 10, 2017
*/
public interface SchemaValidated {
}

View File

@@ -0,0 +1,114 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.Provider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.ValidationMessage;
/**
*
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 6 Dec 2024
*/
@Provider
@Consumes(value = {
MediaType.APPLICATION_JSON, "application/json; charset=utf-8"
})
public class ValidatedMessageBodyReader implements MessageBodyReader<SchemaValidated> {
private static final Log LOG = LogFactory.getLog(ValidatedMessageBodyReader.class);
/* (non-Javadoc)
* @see jakarta.ws.rs.ext.MessageBodyReader#isReadable(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType)
*/
@Override
public boolean isReadable(Class<?> classType, Type type, Annotation[] annotations, MediaType mediaType) {
if (mediaType.getType().contains("application/json")) {
return false;
}
return Arrays.stream(annotations).anyMatch(a -> a.annotationType() == JsonSchemaValidate.class);
}
/* (non-Javadoc)
* @see jakarta.ws.rs.ext.MessageBodyReader#readFrom(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType, jakarta.ws.rs.core.MultivaluedMap, java.io.InputStream)
*/
@Override
public SchemaValidated readFrom(Class<SchemaValidated> classType, Type type, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream input) throws IOException {
final String jsonData = read(input);
Optional<JsonSchemaValidate> annotation = Arrays.stream(annotations).filter(a -> a.annotationType() == JsonSchemaValidate.class).map(a -> (JsonSchemaValidate) a).findAny();
if (annotation.isPresent()) {
ValidationReply reply = validate(annotation.get(), jsonData);
if (!reply.success) {
throw new WebApplicationException(reply.getErrorResponse());
}
}
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(new StringReader(jsonData), classType);
}
/**
* @param jsonSchema
* @param jsonData
* @return
*/
private ValidationReply validate(JsonSchemaValidate jsonSchema, String jsonData) {
String schemaPath = jsonSchema.value();
try {
JsonSchema schema = getJsonSchema(schemaPath);
JsonNode node = getJsonNode(jsonData);
Set<ValidationMessage> errors = schema.validate(node);
if (!errors.isEmpty()) {
LOG.error("Failed to validate json to schema " + schemaPath);
errors.stream().forEach(LOG::error);
}
return new ValidationReply.Builder().success(errors.isEmpty()).errors(errors).build();
} catch (IOException e) {
LOG.error(e.getMessage(), e);
return new ValidationReply.Builder().success(false).build();
}
}
protected JsonSchema getJsonSchema(String name) throws IOException {
JsonSchemaFactory factory = new JsonSchemaFactory();
try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);) {
return factory.getSchema(input);
}
}
protected JsonNode getJsonNode(String content) throws IOException {
return new ObjectMapper().readTree(content);
}
private String read(InputStream input) throws IOException {
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input, Charset.forName("UTF-8")))) {
return buffer.lines().collect(Collectors.joining("\n"));
}
}
}

View File

@@ -0,0 +1,51 @@
package marketing.heyday.hartmann.fotodocumentation.rest.jackson;
import java.util.HashSet;
import java.util.Set;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import com.networknt.schema.ValidationMessage;
/**
*
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Feb 14, 2017
*/
public final class ValidationReply {
public final boolean success;
public final Set<ValidationMessage> errors = new HashSet<>();
private ValidationReply(boolean success, Set<ValidationMessage> errors) {
this.success = success;
this.errors.addAll(errors);
}
public Response getErrorResponse() {
return Response.status(Status.BAD_REQUEST).entity(errors).build();
}
public static final class Builder {
private boolean success;
private Set<ValidationMessage> errors = new HashSet<>();
public Builder success(boolean success) {
this.success = success;
return this;
}
public Builder errors(Set<ValidationMessage> errors) {
this.errors = errors;
return this;
}
public ValidationReply build() {
return new ValidationReply(success, errors);
}
}
}

View File

@@ -0,0 +1,17 @@
package marketing.heyday.hartmann.fotodocumentation.rest.vo;
import java.util.Date;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 19 Jan 2026
*/
public record CustomerPictureValue(String username, String pharmacyName, String customerNumber, Date date, String comment, String base64String) {
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0" bean-discovery-mode="all">
</beans>

View File

@@ -0,0 +1,24 @@
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
<persistence-unit name="auth" transaction-type="JTA">
<jta-data-source>java:/jdbc/fotoDocumentationDS</jta-data-source>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Right</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.User</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Customer</class>
<class>marketing.heyday.hartmann.fotodocumentation.core.model.Picture</class>
<properties>
<property name="hibernate.format_sql" value="false" />
<property name="hibernate.show_sql" value="false" />
<!-- <property name="hibernate.archive.autodetection" value="class" /> -->
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
<property name="hibernate.hbm2ddl.auto" value="none" />
<property name="hibernate.jpa.compliance.query" value="false" />
</properties>
</persistence-unit>
</persistence>

View File

@@ -0,0 +1,76 @@
-- Right
create sequence right_seq start 25;
create table x_right (
right_id bigint PRIMARY KEY,
code varchar(50) NOT NULL,
name varchar(150) NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_x_right_code UNIQUE(code)
);
-- user
create sequence user_seq start 25;
create table x_user (
user_id bigint PRIMARY KEY,
username varchar(150) NOT NULL,
password varchar(150) NOT NULL,
salt varchar(150) NOT NULL,
title varchar(15) ,
firstname varchar(150) NOT NULL,
lastname varchar(150) NOT NULL,
email varchar(150) NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_x_user_username UNIQUE(username)
);
create table user_to_right (
user_id_fk bigint REFERENCES x_user,
right_id_fk bigint REFERENCES x_right,
PRIMARY KEY(user_id_fk, right_id_fk)
);
-- customer
create sequence customer_seq start 25;
create table customer (
customer_id bigint PRIMARY KEY,
customer_number varchar(150) NOT NULL,
name varchar(150) NOT NULL,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
CONSTRAINT unq_customer_number UNIQUE(customer_number)
);
-- picture
create sequence picture_seq start 25;
create table picture (
picture_id bigint PRIMARY KEY,
username varchar(150),
picture_date timestamp NOT NULL,
comment TEXT,
image TEXT,
jpa_active boolean NOT NULL,
jpa_created timestamp NOT NULL,
jpa_updated timestamp NOT NULL,
jpa_version integer NOT NULL,
customer_id_fk bigint REFERENCES customer
);

View File

@@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Add Customer Picture",
"description": "Add a Customer Picture to the system",
"type": "object",
"properties": {
"username": {
"description": "The username from the user who uploads the picture",
"type": "string"
},
"pharmacyName": {
"description": "The Name from the pharmacy customer ",
"type": "string"
},
"customerNumber": {
"description": "The unique number from the pharmacy customer ",
"type": "string"
},
"date": {
"description": "The date when the picture is taken ",
"type": "string"
},
"comment": {
"description": "A free text comment field ",
"type": "string"
},
"base64String": {
"description": "The Picture content as base64 ",
"type": "string"
}
},
"required": [
"username",
"pharmacyName",
"customerNumber",
"date",
"comment",
"base64String"
]
}

View File

@@ -0,0 +1,16 @@
{
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
},
"petstore_auth": {
"type": "oauth2",
"authorizationUrl": "http://swagger.io/api/oauth/dialog",
"flow": "implicit",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
}

View File

@@ -0,0 +1,107 @@
#{{#info}}{{title}}
## {{join schemes " | "}}://{{host}}{{basePath}}
{{description}}
{{#contact}}
[**Contact the developer**](mailto:{{email}})
{{/contact}}
**Version** {{version}}
[**Terms of Service**]({{termsOfService}})
{{#license}}[**{{name}}**]({{url}}){{/license}}
{{/info}}
{{#if consumes}}**Consumes:** {{join consumes ", "}}{{/if}}
{{#if produces}}**Produces:** {{join produces ", "}}{{/if}}
{{#if securityDefinitions}}
# Security Definitions
{{/if}}
{{> security}}
# APIs
{{#each paths}}
## {{@key}}
{{#this}}
{{#get}}
### GET
{{> operation}}
{{/get}}
{{#put}}
### PUT
{{> operation}}
{{/put}}
{{#post}}
### POST
{{> operation}}
{{/post}}
{{#delete}}
### DELETE
{{> operation}}
{{/delete}}
{{#option}}
### OPTION
{{> operation}}
{{/option}}
{{#patch}}
### PATCH
{{> operation}}
{{/patch}}
{{#head}}
### HEAD
{{> operation}}
{{/head}}
{{/this}}
{{/each}}
# Definitions
{{#each definitions}}
## <a name="/definitions/{{key}}">{{@key}}</a>
<table border="1">
<tr>
<th>name</th>
<th>type</th>
<th>required</th>
<th>description</th>
<th>example</th>
</tr>
{{#each this.properties}}
<tr>
<td>{{@key}}</td>
<td>
{{#ifeq type "array"}}
{{#items.$ref}}
{{type}}[<a href="{{items.$ref}}">{{basename items.$ref}}</a>]
{{/items.$ref}}
{{^items.$ref}}{{type}}[{{items.type}}]{{/items.$ref}}
{{else}}
{{#$ref}}<a href="{{$ref}}">{{basename $ref}}</a>{{/$ref}}
{{^$ref}}{{type}}{{#format}} ({{format}}){{/format}}{{/$ref}}
{{/ifeq}}
</td>
<td>{{#required}}required{{/required}}{{^required}}optional{{/required}}</td>
<td>{{#description}}{{{description}}}{{/description}}{{^description}}-{{/description}}</td>
<td>{{example}}</td>
</tr>
{{/each}}
</table>
{{/each}}

View File

@@ -0,0 +1,81 @@
{{#deprecated}}-deprecated-{{/deprecated}}
<a id="{{operationId}}">{{summary}}</a>
{{{description}}}
{{#if externalDocs.url}}{{externalDocs.description}}. [See external documents for more details]({{externalDocs.url}})
{{/if}}
{{#if security}}
#### Security
{{/if}}
{{#security}}
{{#each this}}
* {{@key}}
{{#this}} * {{this}}
{{/this}}
{{/each}}
{{/security}}
#### Request
{{#if consumes}}
**Content-Type: ** {{join consumes ", "}}{{/if}}
##### Parameters
{{#if parameters}}
<table border="1">
<tr>
<th>Name</th>
<th>Located in</th>
<th>Required</th>
<th>Description</th>
<th>Default</th>
<th>Schema</th>
<th>Example</th>
</tr>
{{/if}}
{{#parameters}}
<tr>
<th>{{name}}</th>
<td>{{in}}</td>
<td>{{#if required}}yes{{else}}no{{/if}}</td>
<td>{{description}}{{#if pattern}} (**Pattern**: `{{pattern}}`){{/if}}</td>
<td> - </td>
{{#ifeq in "body"}}
<td>
{{#ifeq schema.type "array"}}Array[<a href="{{schema.items.$ref}}">{{basename schema.items.$ref}}</a>]{{/ifeq}}
{{#schema.$ref}}<a href="{{schema.$ref}}">{{basename schema.$ref}}</a> {{/schema.$ref}}
</td>
{{else}}
{{#ifeq type "array"}}
<td>Array[{{items.type}}] ({{collectionFormat}})</td>
{{else}}
<td>{{type}} {{#format}}({{format}}){{/format}}</td>
{{/ifeq}}
{{/ifeq}}
<td>
{{#each examples}}
{{{this}}}
{{/each}}
</td>
</tr>
{{/parameters}}
{{#if parameters}}
</table>
{{/if}}
#### Response
{{#if produces}}**Content-Type: ** {{join produces ", "}}{{/if}}
| Status Code | Reason | Response Model |
|-------------|-------------|----------------|
{{#each responses}}| {{@key}} | {{description}} | {{#schema.$ref}}<a href="{{schema.$ref}}">{{basename schema.$ref}}</a>{{/schema.$ref}}{{^schema.$ref}}{{#ifeq schema.type "array"}}Array[<a href="{{schema.items.$ref}}">{{basename schema.items.$ref}}</a>]{{else}}{{schema.type}}{{/ifeq}}{{/schema.$ref}}{{^schema}} - {{/schema}}|
{{/each}}

View File

@@ -0,0 +1,88 @@
{{#each securityDefinitions}}
### {{@key}}
{{#this}}
{{#ifeq type "oauth2"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
{{#if authorizationUrl}}
<tr>
<th>authorizationUrl</th>
<th colspan="2">{{authorizationUrl}}</th>
</tr>
{{/if}}
{{#if flow}}
<tr>
<th>flow</th>
<th colspan="2">{{flow}}</th>
</tr>
{{/if}}
{{#if tokenUrl}}
<tr>
<th>tokenUrl</th>
<th colspan="2">{{tokenUrl}}</th>
</tr>
{{/if}}
{{#if scopes}}
<tr>
<td rowspan="3">scopes</td>
{{#each scopes}}
<td>{{@key}}</td>
<td>{{this}}</td>
</tr>
<tr>
{{/each}}
</tr>
{{/if}}
</table>
{{/ifeq}}
{{#ifeq type "apiKey"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
{{#if name}}
<tr>
<th>name</th>
<th colspan="2">{{name}}</th>
</tr>
{{/if}}
{{#if in}}
<tr>
<th>in</th>
<th colspan="2">{{in}}</th>
</tr>
{{/if}}
</table>
{{/ifeq}}
{{#ifeq type "basic"}}
<table>
<tr>
<th>type</th>
<th colspan="2">{{type}}</th>
</tr>
{{#if description}}
<tr>
<th>description</th>
<th colspan="2">{{description}}</th>
</tr>
{{/if}}
</table>
{{/ifeq}}
{{/this}}
{{/each}}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<title>API Document</title>
<xmp theme="united" style="display:none;">
{{>markdown}}
</xmp>
<script src="http://strapdownjs.com/v/0.2/strapdown.js"></script>
<!-- code for TOC (jquery plugin) (see http://projects.jga.me/toc/#toc0) -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://rawgit.com/jgallen23/toc/master/dist/toc.min.js"></script>
<script src="https://rawgit.com/zipizap/strapdown_template/master/js/init_TOC.js"></script>
</html>

View File

@@ -0,0 +1,19 @@
package org.mockito.configuration;
/**
*
* <p>Copyright: Copyright (c) 2016</p>
* <p>Company: heyday marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: Oct 14, 2016
*/
public class MockitoConfiguration extends DefaultMockitoConfiguration {
@Override
public boolean enableClassCache() {
return false;
}
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out"/>
<param name="Threshold" value="DEBUG"/>
<layout class="org.apache.log4j.PatternLayout">
<!-- The default pattern: Date Priority [Category] Message\n -->
<param name="ConversionPattern" value="%d{ABSOLUTE} %-5p [%c] %m%n"/>
</layout>
</appender>
<!-- ================ -->
<!-- Limit categories -->
<!-- ================ -->
<!-- Limit the org.apache category to INFO as its DEBUG is verbose -->
<category name="org.apache">
<priority value="INFO"/>
</category>
<category name="com.bm">
<priority value="INFO"/>
</category>
<category name="com.bm.introspectors">
<priority value="ERROR"/>
</category>
<category name="org.hibernate.cfg.annotations">
<priority value="WARN"/>
</category>
<category name="org.hibernate.cfg">
<priority value="WARN"/>
</category>
<category name="org.hibernate.tool">
<priority value="WARN"/>
</category>
<category name="org.hibernate.validator">
<priority value="WARN"/>
</category>
<category name="org.hibernate">
<priority value="ERROR"/>
</category>
<category name="org.dbunit">
<priority value="DEBUG"/>
</category>
<category name="de.juwimm">
<priority value="DEBUG"/>
</category>
<category name="STDOUT">
<priority value="DEBUG"/>
</category>
<root>
<appender-ref ref="CONSOLE"/>
</root>
</log4j:configuration>