diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..4bfca3250 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM eclipse-temurin:17-jre +EXPOSE 8082 + +ENV OTEL_SERVICE_NAME=PetClinic +ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:5050 +ENV OTEL_LOGS_EXPORTER="otlp" +ENV OTEL_METRICS_EXPORTER=none + +ENV CODE_PACKAGE_PREFIXES="org.springframework.samples.petclinic" +ENV DEPLOYMENT_ENV="SAMPLE_ENV" + +ADD target/spring-petclinic-*.jar /app.jar +ADD build/otel/opentelemetry-javaagent.jar /opentelemetry-javaagent.jar +ADD build/otel/digma-otel-agent-extension.jar /digma-otel-agent-extension.jar + +HEALTHCHECK --interval=20s --timeout=3s --start-period=10s --retries=4 \ + CMD curl -f http://localhost:8082/ || exit 1 + +ENTRYPOINT java -jar -javaagent:/opentelemetry-javaagent.jar -Dotel.javaagent.extensions=/digma-otel-agent-extension.jar app.jar diff --git a/digma-otel-agent-extension.jar b/digma-otel-agent-extension.jar new file mode 100644 index 000000000..366f6c05a Binary files /dev/null and b/digma-otel-agent-extension.jar differ diff --git a/docker-compose.override.otel.yml b/docker-compose.override.otel.yml new file mode 100644 index 000000000..1600ec9c3 --- /dev/null +++ b/docker-compose.override.otel.yml @@ -0,0 +1,15 @@ +version: '3' + +services: + + pet-clinic: + volumes: + - "./otel/opentelemetry-javaagent.jar:/otel/opentelemetry-javaagent.jar" + - "./otel/digma-otel-agent-extension.jar:/otel/digma-otel-agent-extension.jar" + + environment: + - JAVA_TOOL_OPTIONS=-javaagent:/otel/opentelemetry-javaagent.jar -Dotel.exporter.otlp.endpoint=http://host.docker.internal:5050 -Dotel.javaagent.extensions=/otel/digma-otel-agent-extension.jar + - OTEL_SERVICE_NAME=pet-clinic + - DEPLOYMENT_ENV=LOCAL_DOCKER + extra_hosts: + - "host.docker.internal:host-gateway" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9c34d2a33..0edf09743 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,18 @@ -version: "2.2" +version: '3' services: + pet-clinic: + build: ./ + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8082/" ] + interval: 20s + timeout: 10s + retries: 4 + start_period: 5s + ports: + - "8082:8082" + entrypoint: java -jar app.jar + mysql: image: mysql:8.0 ports: diff --git a/opentelemetry-javaagent.jar b/opentelemetry-javaagent.jar new file mode 100644 index 000000000..6325d4939 Binary files /dev/null and b/opentelemetry-javaagent.jar differ diff --git a/otel/digma-otel-agent-extension.jar b/otel/digma-otel-agent-extension.jar new file mode 100644 index 000000000..366f6c05a Binary files /dev/null and b/otel/digma-otel-agent-extension.jar differ diff --git a/otel/opentelemetry-javaagent.jar b/otel/opentelemetry-javaagent.jar new file mode 100644 index 000000000..6325d4939 Binary files /dev/null and b/otel/opentelemetry-javaagent.jar differ diff --git a/petshop-chart/.helmignore b/petshop-chart/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/petshop-chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/petshop-chart/Chart.yaml b/petshop-chart/Chart.yaml new file mode 100644 index 000000000..26f48a0d2 --- /dev/null +++ b/petshop-chart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: petshop-chart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.3 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/petshop-chart/templates/NOTES.txt b/petshop-chart/templates/NOTES.txt new file mode 100644 index 000000000..cc74ea800 --- /dev/null +++ b/petshop-chart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "petshop-chart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "petshop-chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "petshop-chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "petshop-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/petshop-chart/templates/_helpers.tpl b/petshop-chart/templates/_helpers.tpl new file mode 100644 index 000000000..fbb6d6949 --- /dev/null +++ b/petshop-chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "petshop-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "petshop-chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "petshop-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "petshop-chart.labels" -}} +helm.sh/chart: {{ include "petshop-chart.chart" . }} +{{ include "petshop-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "petshop-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "petshop-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "petshop-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "petshop-chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/petshop-chart/templates/deployment.yaml b/petshop-chart/templates/deployment.yaml new file mode 100644 index 000000000..778743403 --- /dev/null +++ b/petshop-chart/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "petshop-chart.fullname" . }} + labels: + {{- include "petshop-chart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "petshop-chart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "petshop-chart.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "petshop-chart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8082 + protocol: TCP + env: + - name: "OTEL_SERVICE_NAME" + value: "PetClinicWithAgent" + - name: "OTEL_EXPORTER_OTLP_ENDPOINT" + value: "http://digma-collector-api.digma.svc.cluster.local:5050" + - name: "OTEL_LOGS_EXPORTER" + value: "otlp" + - name: "CODE_PACKAGE_PREFIXES" + value: "org.springframework.samples.petclinic" + - name: "DEPLOYMENT_ENV" + value: "Load test" + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/petshop-chart/templates/hpa.yaml b/petshop-chart/templates/hpa.yaml new file mode 100644 index 000000000..0f41df052 --- /dev/null +++ b/petshop-chart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "petshop-chart.fullname" . }} + labels: + {{- include "petshop-chart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "petshop-chart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/petshop-chart/templates/ingress.yaml b/petshop-chart/templates/ingress.yaml new file mode 100644 index 000000000..8d2770aa4 --- /dev/null +++ b/petshop-chart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "petshop-chart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "petshop-chart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/petshop-chart/templates/service.yaml b/petshop-chart/templates/service.yaml new file mode 100644 index 000000000..baea99871 --- /dev/null +++ b/petshop-chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "petshop-chart.fullname" . }} + labels: + {{- include "petshop-chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8082 + protocol: TCP + name: api + selector: + {{- include "petshop-chart.selectorLabels" . | nindent 4 }} diff --git a/petshop-chart/templates/serviceaccount.yaml b/petshop-chart/templates/serviceaccount.yaml new file mode 100644 index 000000000..2c0c159e2 --- /dev/null +++ b/petshop-chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "petshop-chart.serviceAccountName" . }} + labels: + {{- include "petshop-chart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/petshop-chart/templates/tests/test-connection.yaml b/petshop-chart/templates/tests/test-connection.yaml new file mode 100644 index 000000000..f38ce919c --- /dev/null +++ b/petshop-chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "petshop-chart.fullname" . }}-test-connection" + labels: + {{- include "petshop-chart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "petshop-chart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/petshop-chart/values.yaml b/petshop-chart/values.yaml new file mode 100644 index 000000000..0b5dcf1cf --- /dev/null +++ b/petshop-chart/values.yaml @@ -0,0 +1,82 @@ +# Default values for petshop-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: digmaai/petshop-app-deploy + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: 0.5.5 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: LoadBalancer + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pom.xml b/pom.xml index 3f330a9ee..b45f3a66c 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,11 @@ postgresql runtime - + + io.micrometer + micrometer-registry-prometheus + runtime + javax.cache @@ -106,7 +110,11 @@ ${webjars-font-awesome.version} - + + com.squareup.okhttp3 + okhttp + 4.9.1 + org.springframework.boot spring-boot-devtools @@ -117,6 +125,16 @@ jakarta.xml.bind jakarta.xml.bind-api + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-annotations + 1.26.0 + + + com.vaadin.external.google + android-json + 0.0.20131108.vaadin1 + diff --git a/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java new file mode 100644 index 000000000..737bb3616 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationService.java @@ -0,0 +1,14 @@ +package org.springframework.samples.petclinic.adapters; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import org.json.JSONException; + +import java.io.IOException; + +public interface PetVaccinationService { + @WithSpan + VaccinnationRecord[] AllVaccines() throws JSONException, IOException; + + @WithSpan + VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException; +} diff --git a/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java new file mode 100644 index 000000000..b1b1737ab --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/adapters/PetVaccinationServiceFacade.java @@ -0,0 +1,74 @@ +package org.springframework.samples.petclinic.adapters; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; + +@Component +public class PetVaccinationServiceFacade implements PetVaccinationService { + + public static final String VACCINES_RECORDS_URL = "https://647f4bb4c246f166da9084c7.mockapi.io/api/vetcheck/vaccines"; + + private String MakeHttpCall(String url) throws IOException{ + + Request getAllVaccinesRequest = new Request.Builder().url(url).build(); + OkHttpClient client = new OkHttpClient(); + Response getAllVaccinesResult = client.newCall(getAllVaccinesRequest).execute(); + return getAllVaccinesResult.body().string(); + } + + @Override + @WithSpan + public VaccinnationRecord[] AllVaccines() throws JSONException, IOException { + + var vaccineListString = MakeHttpCall(VACCINES_RECORDS_URL); + JSONArray jArr = new JSONArray(vaccineListString); + var vaccinnationRecords = + new ArrayList(); + + for (int i = 0; i < jArr.length(); i++) { + + VaccinnationRecord record = parseVaccinationRecord(jArr.getJSONObject(i)); + vaccinnationRecords.add(record); + + } + return vaccinnationRecords.toArray(VaccinnationRecord[]::new); + + } + + @Override + @WithSpan + public VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException { + + var idUrl = VACCINES_RECORDS_URL + "/" + vaccinationRecordId; + + var vaccineListString = MakeHttpCall(idUrl); + + JSONObject vaccineJson = new JSONObject(vaccineListString); + return parseVaccinationRecord(vaccineJson); + + } + + @NotNull + private static VaccinnationRecord parseVaccinationRecord(JSONObject jsonObject) throws JSONException { + Integer petId = jsonObject.getInt("pet_id"); + Integer id = jsonObject.getInt("id"); + String vaccineDateString = jsonObject.getString("vaccine_date"); + var vaccineDate = Instant.parse(vaccineDateString); + return new VaccinnationRecord(id, petId, vaccineDate); + } + + + +} diff --git a/src/main/java/org/springframework/samples/petclinic/adapters/VaccinnationRecord.java b/src/main/java/org/springframework/samples/petclinic/adapters/VaccinnationRecord.java new file mode 100644 index 000000000..f512f4121 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/adapters/VaccinnationRecord.java @@ -0,0 +1,5 @@ +package org.springframework.samples.petclinic.adapters; + +public record VaccinnationRecord(int recordId, int petId, java.time.Instant vaccineDate) { + +} diff --git a/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java b/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java new file mode 100644 index 000000000..e11a19867 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/domain/OwnerValidation.java @@ -0,0 +1,172 @@ +package org.springframework.samples.petclinic.domain; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import org.springframework.samples.petclinic.owner.Owner; + +import java.util.concurrent.ThreadLocalRandom; + +public class OwnerValidation { + + private int counter = 0; + + private UserValidationService usrValSvc; + + private PasswordUtils pwdUtils; + + private Tracer otelTracer; + + private RoleService roleSvc; + + private TwoFactorAuthenticationService twoFASvc; + + public OwnerValidation(Tracer otelTracer) { + this.pwdUtils = new PasswordUtils(); + this.roleSvc = new RoleService(); + this.otelTracer = otelTracer; + this.usrValSvc = new UserValidationService(); + this.twoFASvc = new TwoFactorAuthenticationService(); + } + + @WithSpan + public void ValidateOwnerWithExternalService(Owner owner) { + + this.AuthServiceValidateUser(owner); + } + + @WithSpan + private void NewFunction() { + + } + + @WithSpan + public boolean UserNameMustStartWithR(String usr) { + + if (!usr.toLowerCase().startsWith("r")) { + return false; + } + + return true; + + } + + @WithSpan + // This function and classes were generated by ChatGPT + public boolean ValidateUserAccess(String usr, String pswd, String sysCode) { + + UserNameMustStartWithR(usr); + boolean vldUsr = usrValSvc.vldtUsr(usr); + if (!vldUsr) { + return false; + } + + + boolean vldPswd = pwdUtils.vldtPswd(usr, pswd); + if (!vldPswd) { + return false; + } + + boolean vldUsrRole = roleSvc.vldtUsrRole(usr, sysCode); + if (!vldUsrRole) { + return false; + } + + boolean is2FASuccess = twoFASvc.init2FA(usr); + if (!is2FASuccess) { + return false; + } + + boolean is2FATokenValid = false; + int retry = 0; + while (retry < 3 && !is2FATokenValid) { + String token = twoFASvc.getTokenInput(); + is2FATokenValid = twoFASvc.vldtToken(usr, token); + retry++; + } + + if (!is2FATokenValid) { + return false; + } + + return true; + } + + @WithSpan + private synchronized void AuthServiceValidateUser(Owner owner) { + // This is the actual Root Cause!! + try { + Thread.sleep(4200 + ThreadLocalRandom.current().nextInt(90, 1100 + 1)); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @WithSpan + public boolean checkOwnerValidity(Owner owner) { + + this.ValidateOwnerUserBad(owner); + return ValidateOwnerUser(owner); + + } + + @WithSpan + private boolean ValidateOwnerUserBad(Owner owner) { + { + + for (int i = 0; i < 100; i++) { + ValidateOwner(); + } + return true; + + } + } + + @WithSpan + private boolean ValidateOwnerUser(Owner owner) { + + Span span = otelTracer.spanBuilder("db_access_01").startSpan(); + + var max = ThreadLocalRandom.current().nextInt(90, 110 + 1); + try { + for (int i = 0; i < max; i++) { + ValidateOwner(); + } + } + finally { + span.end(); + } + return true; + + } + + @WithSpan + private void ValidateOwner() { + // simulate SpanKind of DB query + // see + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Span span = otelTracer.spanBuilder("query_users_by_id") + .setSpanKind(SpanKind.CLIENT) + .setAttribute("db.system", "other_sql") + .setAttribute("db.statement", "select * from users where id = :id") + .startSpan(); + + try { + Thread.sleep(14); + } + catch (Exception e) { + + } + finally { + span.end(); + } + } + + @WithSpan + public void PerformValidationFlow(Owner owner) { + + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/domain/PasswordUtils.java b/src/main/java/org/springframework/samples/petclinic/domain/PasswordUtils.java new file mode 100644 index 000000000..ff95380e0 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/domain/PasswordUtils.java @@ -0,0 +1,29 @@ +package org.springframework.samples.petclinic.domain; + +import io.opentelemetry.instrumentation.annotations.WithSpan; + +public class PasswordUtils { + + @WithSpan + public boolean vldtPswd(String usr, String pswd) { + try { + Thread.sleep(1); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return true; + } + + @WithSpan + public String encPswd(String pswd) { + try { + Thread.sleep(300); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return ""; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java b/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java new file mode 100644 index 000000000..ca8b34f4b --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/domain/PetVaccinationStatusService.java @@ -0,0 +1,48 @@ +package org.springframework.samples.petclinic.domain; + +import io.opentelemetry.api.trace.Span; +import org.json.JSONException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.samples.petclinic.adapters.PetVaccinationService; +import org.springframework.samples.petclinic.adapters.PetVaccinationServiceFacade; +import org.springframework.samples.petclinic.adapters.VaccinnationRecord; +import org.springframework.samples.petclinic.owner.Pet; +import org.springframework.samples.petclinic.owner.PetVaccine; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Component +public class PetVaccinationStatusService { + + @Autowired + private PetVaccinationService adapter; + + public void UpdateVaccinationStatus(Pet[] pets){ + + for (Pet pet: pets){ + try { + var vaccinationRecords = this.adapter.AllVaccines(); + for (VaccinnationRecord record : vaccinationRecords){ + + var recordInfo = this.adapter.VaccineRecord(record.recordId()); + if (recordInfo.petId()==pet.getId()){ + var date = LocalDateTime.ofInstant(recordInfo.vaccineDate(), ZoneId.systemDefault()); + PetVaccine petVaccine = new PetVaccine(); + petVaccine.setDate(date.toLocalDate()); + pet.addVaccine(petVaccine); + } + } + + } catch (JSONException |IOException e) { + //Fail silently + Span.current().recordException(e); + } + + } + + + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/domain/RoleService.java b/src/main/java/org/springframework/samples/petclinic/domain/RoleService.java new file mode 100644 index 000000000..02194fd73 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/domain/RoleService.java @@ -0,0 +1,18 @@ +package org.springframework.samples.petclinic.domain; + +import io.opentelemetry.instrumentation.annotations.WithSpan; + +public class RoleService { + + @WithSpan + public boolean vldtUsrRole(String usr, String sysCode) { + try { + Thread.sleep(40); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return true; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/domain/TwoFactorAuthenticationService.java b/src/main/java/org/springframework/samples/petclinic/domain/TwoFactorAuthenticationService.java new file mode 100644 index 000000000..87e4998ef --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/domain/TwoFactorAuthenticationService.java @@ -0,0 +1,33 @@ +package org.springframework.samples.petclinic.domain; + +import io.opentelemetry.instrumentation.annotations.WithSpan; + +public class TwoFactorAuthenticationService { + + @WithSpan + public boolean init2FA(String usr) { + try { + Thread.sleep(400); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return true; + } + + public String getTokenInput() { + return ""; + } + + public boolean vldtToken(String usr, String token) { + try { + Thread.sleep(40); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return true; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/domain/UserValidationService.java b/src/main/java/org/springframework/samples/petclinic/domain/UserValidationService.java new file mode 100644 index 000000000..151297c97 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/domain/UserValidationService.java @@ -0,0 +1,19 @@ +package org.springframework.samples.petclinic.domain; + +import io.opentelemetry.instrumentation.annotations.WithSpan; + +public class UserValidationService { + + @WithSpan + public boolean vldtUsr(String usr) { + + try { + Thread.sleep(300); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return true; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Owner.java b/src/main/java/org/springframework/samples/petclinic/owner/Owner.java index ac556459d..cc324705f 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Owner.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Owner.java @@ -15,7 +15,11 @@ */ package org.springframework.samples.petclinic.owner; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.springframework.core.style.ToStringCreator; @@ -124,6 +128,27 @@ public class Owner extends Person { return null; } + public boolean isVaccineExpired() { + + for (Pet pet : getPets()) { + + if (!pet.isNew()) { + + for (PetVaccine vaccine : pet.getPetVaccines()) { + + Duration duration = Duration.between(LocalDateTime.now(), vaccine.getDate()); + if (duration.toDays() > 360) { + return true; + } + + } + + } + + } + return false; + } + /** * Return the Pet with the given name, or null if none found for this Owner. * @param name to test diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java index c91a94c93..f77ce4d28 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java @@ -18,9 +18,11 @@ package org.springframework.samples.petclinic.owner; import java.util.List; import java.util.Map; +import io.opentelemetry.instrumentation.annotations.WithSpan; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.samples.petclinic.domain.OwnerValidation; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -35,6 +37,8 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.validation.Valid; +import static io.opentelemetry.api.GlobalOpenTelemetry.getTracer; + /** * @author Juergen Hoeller * @author Ken Krebs @@ -48,8 +52,14 @@ class OwnerController { private final OwnerRepository owners; + private OwnerValidation validator; + public OwnerController(OwnerRepository clinicService) { this.owners = clinicService; + var otelTracer = getTracer("OwnerController"); + + validator = new OwnerValidation(otelTracer); + } @InitBinder @@ -64,7 +74,10 @@ class OwnerController { @GetMapping("/owners/new") public String initCreationForm(Map model) { + Owner owner = new Owner(); + validator.ValidateOwnerWithExternalService(owner); + model.put("owner", owner); return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } @@ -74,8 +87,12 @@ class OwnerController { if (result.hasErrors()) { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } + validator.ValidateOwnerWithExternalService(owner); + validator.PerformValidationFlow(owner); + validator.checkOwnerValidity(owner); this.owners.save(owner); + validator.ValidateUserAccess("admin", "pwd", "fullaccess"); return "redirect:/owners/" + owner.getId(); } @@ -87,6 +104,8 @@ class OwnerController { @GetMapping("/owners") public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result, Model model) { + validator.ValidateUserAccess("admin", "pwd", "fullaccess"); + // allow parameterless GET request for /owners to return all records if (owner.getLastName() == null) { owner.setLastName(""); // empty string signifies broadest possible search @@ -110,6 +129,11 @@ class OwnerController { return addPaginationModel(page, model, ownersResults); } + + + + + private String addPaginationModel(int page, Model model, Page paginated) { model.addAttribute("listOwners", paginated); List listOwners = paginated.getContent(); @@ -120,12 +144,18 @@ class OwnerController { return "owners/ownersList"; } + @WithSpan private Page findPaginatedForOwnersLastName(int page, String lastname) { - int pageSize = 5; + int pageSize = 25; Pageable pageable = PageRequest.of(page - 1, pageSize); return owners.findByLastName(lastname, pageable); } + + + + + @GetMapping("/owners/{ownerId}/edit") public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) { Owner owner = this.owners.findById(ownerId); @@ -133,6 +163,24 @@ class OwnerController { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } + + + + + + + + + + + + + + + + + + @PostMapping("/owners/{ownerId}/edit") public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId) { @@ -151,8 +199,10 @@ class OwnerController { * @return a ModelMap with the model attributes for the view */ @GetMapping("/owners/{ownerId}") - public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { - ModelAndView mav = new ModelAndView("owners/ownerDetails"); + public ModelAndView showOwner(@PathVariable("ownerId") + int ownerId) { + ModelAndView mav = + new ModelAndView("owners/ownerDetails"); Owner owner = this.owners.findById(ownerId); mav.addObject(owner); return mav; diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Pet.java b/src/main/java/org/springframework/samples/petclinic/owner/Pet.java index 0b0c08ac6..68b2cfe61 100755 --- a/src/main/java/org/springframework/samples/petclinic/owner/Pet.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Pet.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.LinkedHashSet; import java.util.Set; +import io.opentelemetry.instrumentation.annotations.WithSpan; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.samples.petclinic.model.NamedEntity; @@ -57,6 +58,11 @@ public class Pet extends NamedEntity { @OrderBy("visit_date ASC") private Set visits = new LinkedHashSet<>(); + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "pet_id") + @OrderBy("vaccine_date ASC") + private Set pet_vaccines = new LinkedHashSet<>(); + public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; } @@ -77,8 +83,16 @@ public class Pet extends NamedEntity { return this.visits; } + public Collection getPetVaccines() { + return this.pet_vaccines; + } + public void addVisit(Visit visit) { getVisits().add(visit); } + public void addVaccine(PetVaccine vaccine) { + getPetVaccines().add(vaccine); + } + } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java index 9d88f0399..5ab40f7bd 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java @@ -17,6 +17,8 @@ package org.springframework.samples.petclinic.owner; import java.util.Collection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.samples.petclinic.domain.PetVaccinationStatusService; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.StringUtils; @@ -44,6 +46,9 @@ class PetController { private final OwnerRepository owners; + @Autowired + private PetVaccinationStatusService petVaccinationStatus; + public PetController(OwnerRepository owners) { this.owners = owners; } @@ -82,9 +87,20 @@ class PetController { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; } + + + + + + + + + + + @PostMapping("/pets/new") public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model) { - if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) { + if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) { result.rejectValue("name", "duplicate", "already exists"); } @@ -95,9 +111,18 @@ class PetController { } this.owners.save(owner); + petVaccinationStatus.UpdateVaccinationStatus(owner.getPets().toArray(Pet[]::new)); + return "redirect:/owners/{ownerId}"; } + + + + + + + @GetMapping("/pets/{petId}/edit") public String initUpdateForm(Owner owner, @PathVariable("petId") int petId, ModelMap model) { Pet pet = owner.getPet(petId); diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetVaccine.java b/src/main/java/org/springframework/samples/petclinic/owner/PetVaccine.java new file mode 100644 index 000000000..30f573874 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetVaccine.java @@ -0,0 +1,39 @@ +package org.springframework.samples.petclinic.owner; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.samples.petclinic.model.BaseEntity; + +import java.time.LocalDate; + +/** + * Simple JavaBean domain object representing a visit. + * + * @author Ken Krebs + * @author Dave Syer + */ +@Entity +@Table(name = "pet_vaccines") +public class PetVaccine extends BaseEntity { + + @Column(name = "vaccine_date") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate date; + + /** + * Creates a new instance of Visit for the current date + */ + public PetVaccine() { + } + + public LocalDate getDate() { + return this.date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetValidator.java b/src/main/java/org/springframework/samples/petclinic/owner/PetValidator.java index e1370b428..a8c971bf0 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetValidator.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetValidator.java @@ -15,10 +15,18 @@ */ package org.springframework.samples.petclinic.owner; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import okhttp3.*; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import org.springframework.util.StringUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import java.io.IOException; + /** * Validator for Pet forms. *

@@ -51,6 +59,35 @@ public class PetValidator implements Validator { if (pet.getBirthDate() == null) { errors.rejectValue("birthDate", REQUIRED, REQUIRED); } + + try { + updateVaccinationStatus(pet); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + @WithSpan + private boolean updateVaccinationStatus(Pet pet) throws IOException { + // OkHttpClient client = new OkHttpClient(); + // + // Request request = new + // Request.Builder().url("https://647f4bb4c246f166da9084c7.mockapi.io/api/vetcheck/vaccines") + // .build(); + // + // String responseText = ""; + // try (Response response = client.newCall(request).execute()) { + // responseText = response.body().string(); + // JSONObject Jobject = new JSONObject(responseText); + // JSONArray Jarray = Jobject.getJSONArray("employees"); + // + // } + // catch (JSONException e) { + // throw new RuntimeException(e); + // } + return true; + } /** diff --git a/src/main/resources/application-postgres.properties b/src/main/resources/application-postgres.properties index 60889b43c..7f9523d6f 100644 --- a/src/main/resources/application-postgres.properties +++ b/src/main/resources/application-postgres.properties @@ -1,5 +1,5 @@ database=postgres -spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/petclinic} +spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/postgres} spring.datasource.username=${POSTGRES_USER:petclinic} spring.datasource.password=${POSTGRES_PASS:petclinic} # SQL is written to be idempotent so this is safe diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5d3eeed32..d35812dc3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,6 +15,11 @@ spring.messages.basename=messages/messages # Actuator management.endpoints.web.exposure.include=* +management.metrics.distribution.slo.http.server.requests=50ms, 100ms, 200ms, 400ms +management.metrics.distribution.percentiles.http.server.requests=0.5, 0.9, 0.95, 0.99, 0.999 +management.metrics.web.server.request.autotime.percentiles=0.95 +management.metrics.distribution.percentiles-histogram.http.server.requests=true + # Logging logging.level.org.springframework=INFO @@ -23,3 +28,4 @@ logging.level.org.springframework=INFO # Maximum time static resources should be cached spring.web.resources.cache.cachecontrol.max-age=12h +server.port=8082 diff --git a/src/main/resources/db/h2/data.sql b/src/main/resources/db/h2/data.sql index f232b1361..0a20c8cfd 100644 --- a/src/main/resources/db/h2/data.sql +++ b/src/main/resources/db/h2/data.sql @@ -34,6 +34,18 @@ INSERT INTO owners VALUES (default, 'David', 'Schroeder', '2749 Blackhawk Trail' INSERT INTO owners VALUES (default, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); INSERT INTO pets VALUES (default, 'Leo', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Mocka', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Abby', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Tesla', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Freer', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Castro', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Mohawk', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Jerome', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Niley', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Timmy', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (default, 'Bob', '2010-09-07', 1, 1); + + INSERT INTO pets VALUES (default, 'Basil', '2012-08-06', 6, 2); INSERT INTO pets VALUES (default, 'Rosy', '2011-04-17', 2, 3); INSERT INTO pets VALUES (default, 'Jewel', '2010-03-07', 2, 3); diff --git a/src/main/resources/db/h2/schema.sql b/src/main/resources/db/h2/schema.sql index 4a6c322cb..6f1beeb1a 100644 --- a/src/main/resources/db/h2/schema.sql +++ b/src/main/resources/db/h2/schema.sql @@ -62,3 +62,12 @@ CREATE TABLE visits ( ); ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id); CREATE INDEX visits_pet_id ON visits (pet_id); + +CREATE TABLE pet_vaccines ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + pet_id INTEGER, + vaccine_date DATE +); + +ALTER TABLE pet_vaccines ADD CONSTRAINT fk_pet_vaccines_pets FOREIGN KEY (pet_id) REFERENCES pets (id); +CREATE INDEX pet_vaccines_pet_id ON pet_vaccines (pet_id); diff --git a/src/main/resources/db/postgres/data.sql b/src/main/resources/db/postgres/data.sql index 96c9d469f..7635ab389 100644 --- a/src/main/resources/db/postgres/data.sql +++ b/src/main/resources/db/postgres/data.sql @@ -1,53 +1,53 @@ -INSERT INTO vets (first_name, last_name) SELECT 'James', 'Carter' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=1); -INSERT INTO vets (first_name, last_name) SELECT 'Helen', 'Leary' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=2); -INSERT INTO vets (first_name, last_name) SELECT 'Linda', 'Douglas' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=3); -INSERT INTO vets (first_name, last_name) SELECT 'Rafael', 'Ortega' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=4); -INSERT INTO vets (first_name, last_name) SELECT 'Henry', 'Stevens' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=5); -INSERT INTO vets (first_name, last_name) SELECT 'Sharon', 'Jenkins' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=6); + INSERT INTO vets (first_name, last_name) SELECT 'James', 'Carter' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=1); + INSERT INTO vets (first_name, last_name) SELECT 'Helen', 'Leary' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=2); + INSERT INTO vets (first_name, last_name) SELECT 'Linda', 'Douglas' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=3); + INSERT INTO vets (first_name, last_name) SELECT 'Rafael', 'Ortega' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=4); + INSERT INTO vets (first_name, last_name) SELECT 'Henry', 'Stevens' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=5); + INSERT INTO vets (first_name, last_name) SELECT 'Sharon', 'Jenkins' WHERE NOT EXISTS (SELECT * FROM vets WHERE id=6); -INSERT INTO specialties (name) SELECT 'radiology' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='radiology'); -INSERT INTO specialties (name) SELECT 'surgery' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='surgery'); -INSERT INTO specialties (name) SELECT 'dentistry' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='dentistry'); + INSERT INTO specialties (name) SELECT 'radiology' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='radiology'); + INSERT INTO specialties (name) SELECT 'surgery' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='surgery'); + INSERT INTO specialties (name) SELECT 'dentistry' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='dentistry'); -INSERT INTO vet_specialties VALUES (2, 1) ON CONFLICT (vet_id, specialty_id) DO NOTHING; -INSERT INTO vet_specialties VALUES (3, 2) ON CONFLICT (vet_id, specialty_id) DO NOTHING; -INSERT INTO vet_specialties VALUES (3, 3) ON CONFLICT (vet_id, specialty_id) DO NOTHING; -INSERT INTO vet_specialties VALUES (4, 2) ON CONFLICT (vet_id, specialty_id) DO NOTHING; -INSERT INTO vet_specialties VALUES (5, 1) ON CONFLICT (vet_id, specialty_id) DO NOTHING; + INSERT INTO vet_specialties VALUES (2, 1) ON CONFLICT (vet_id, specialty_id) DO NOTHING; + INSERT INTO vet_specialties VALUES (3, 2) ON CONFLICT (vet_id, specialty_id) DO NOTHING; + INSERT INTO vet_specialties VALUES (3, 3) ON CONFLICT (vet_id, specialty_id) DO NOTHING; + INSERT INTO vet_specialties VALUES (4, 2) ON CONFLICT (vet_id, specialty_id) DO NOTHING; + INSERT INTO vet_specialties VALUES (5, 1) ON CONFLICT (vet_id, specialty_id) DO NOTHING; -INSERT INTO types (name) SELECT 'cat' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='cat'); -INSERT INTO types (name) SELECT 'dog' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='dog'); -INSERT INTO types (name) SELECT 'lizard' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='lizard'); -INSERT INTO types (name) SELECT 'snake' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='snake'); -INSERT INTO types (name) SELECT 'bird' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='bird'); -INSERT INTO types (name) SELECT 'hamster' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='cat'); + INSERT INTO types (name) SELECT 'cat' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='cat'); + INSERT INTO types (name) SELECT 'dog' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='dog'); + INSERT INTO types (name) SELECT 'lizard' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='lizard'); + INSERT INTO types (name) SELECT 'snake' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='snake'); + INSERT INTO types (name) SELECT 'bird' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='bird'); + INSERT INTO types (name) SELECT 'hamster' WHERE NOT EXISTS (SELECT * FROM specialties WHERE name='cat'); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=1); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=2); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=3); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=4); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=5); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=6); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=7); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=8); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=9); -INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=10); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=1); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=2); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=3); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=4); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=5); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=6); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=7); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=8); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=9); + INSERT INTO owners (first_name, last_name, address, city, telephone) SELECT 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487' WHERE NOT EXISTS (SELECT * FROM owners WHERE id=10); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Leo', '2000-09-07', 1, 1 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=1); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Basil', '2002-08-06', 6, 2 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=2); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Rosy', '2001-04-17', 2, 3 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=3); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Jewel', '2000-03-07', 2, 3 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=4); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Iggy', '2000-11-30', 3, 4 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=5); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'George', '2000-01-20', 4, 5 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=6); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Samantha', '1995-09-04', 1, 6 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=7); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Max', '1995-09-04', 1, 6 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=8); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Lucky', '1999-08-06', 5, 7 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=9); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Mulligan', '1997-02-24', 2, 8 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=10); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Freddy', '2000-03-09', 5, 9 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=11); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Lucky', '2000-06-24', 2, 10 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=12); -INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Sly', '2002-06-08', 1, 10 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=13); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Leo', '2000-09-07', 1, 1 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=1); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Basil', '2002-08-06', 6, 2 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=2); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Rosy', '2001-04-17', 2, 3 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=3); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Jewel', '2000-03-07', 2, 3 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=4); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Iggy', '2000-11-30', 3, 4 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=5); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'George', '2000-01-20', 4, 5 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=6); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Samantha', '1995-09-04', 1, 6 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=7); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Max', '1995-09-04', 1, 6 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=8); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Lucky', '1999-08-06', 5, 7 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=9); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Mulligan', '1997-02-24', 2, 8 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=10); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Freddy', '2000-03-09', 5, 9 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=11); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Lucky', '2000-06-24', 2, 10 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=12); + INSERT INTO pets (name, birth_date, type_id, owner_id) SELECT 'Sly', '2002-06-08', 1, 10 WHERE NOT EXISTS (SELECT * FROM pets WHERE id=13); -INSERT INTO visits (pet_id, visit_date, description) SELECT 7, '2010-03-04', 'rabies shot' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=1); -INSERT INTO visits (pet_id, visit_date, description) SELECT 8, '2011-03-04', 'rabies shot' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=2); -INSERT INTO visits (pet_id, visit_date, description) SELECT 8, '2009-06-04', 'neutered' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=3); -INSERT INTO visits (pet_id, visit_date, description) SELECT 7, '2008-09-04', 'spayed' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=4); + INSERT INTO visits (pet_id, visit_date, description) SELECT 7, '2010-03-04', 'rabies shot' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=1); + INSERT INTO visits (pet_id, visit_date, description) SELECT 8, '2011-03-04', 'rabies shot' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=2); + INSERT INTO visits (pet_id, visit_date, description) SELECT 8, '2009-06-04', 'neutered' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=3); + INSERT INTO visits (pet_id, visit_date, description) SELECT 7, '2008-09-04', 'spayed' WHERE NOT EXISTS (SELECT * FROM visits WHERE id=4); diff --git a/src/main/resources/db/postgres/schema.sql b/src/main/resources/db/postgres/schema.sql index 1bd582dc2..f09c06f4c 100644 --- a/src/main/resources/db/postgres/schema.sql +++ b/src/main/resources/db/postgres/schema.sql @@ -50,3 +50,9 @@ CREATE TABLE IF NOT EXISTS visits ( description TEXT ); CREATE INDEX ON visits (pet_id); + +CREATE TABLE IF NOT EXISTS pet_vaccines ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + pet_id INT REFERENCES pets (id), + vaccine_date DATE, +); diff --git a/src/main/resources/templates/owners/ownerDetails.html b/src/main/resources/templates/owners/ownerDetails.html index 41f7d1680..a32ed7ef2 100644 --- a/src/main/resources/templates/owners/ownerDetails.html +++ b/src/main/resources/templates/owners/ownerDetails.html @@ -26,6 +26,11 @@ Telephone + + + Needs Vaccine + + Edit diff --git a/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java deleted file mode 100755 index bfaea848d..000000000 --- a/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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. - */ - -package org.springframework.samples.petclinic.owner; - -import org.assertj.core.util.Lists; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.test.web.servlet.MockMvc; - -import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -/** - * Test class for the {@link PetController} - * - * @author Colin But - */ -@WebMvcTest(value = PetController.class, - includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE)) -class PetControllerTests { - - private static final int TEST_OWNER_ID = 1; - - private static final int TEST_PET_ID = 1; - - @Autowired - private MockMvc mockMvc; - - @MockBean - private OwnerRepository owners; - - @BeforeEach - void setup() { - PetType cat = new PetType(); - cat.setId(3); - cat.setName("hamster"); - given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat)); - Owner owner = new Owner(); - Pet pet = new Pet(); - owner.addPet(pet); - pet.setId(TEST_PET_ID); - given(this.owners.findById(TEST_OWNER_ID)).willReturn(owner); - } - - @Test - void testInitCreationForm() throws Exception { - mockMvc.perform(get("/owners/{ownerId}/pets/new", TEST_OWNER_ID)) - .andExpect(status().isOk()) - .andExpect(view().name("pets/createOrUpdatePetForm")) - .andExpect(model().attributeExists("pet")); - } - - @Test - void testProcessCreationFormSuccess() throws Exception { - mockMvc - .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty") - .param("type", "hamster") - .param("birthDate", "2015-02-12")) - .andExpect(status().is3xxRedirection()) - .andExpect(view().name("redirect:/owners/{ownerId}")); - } - - @Test - void testProcessCreationFormHasErrors() throws Exception { - mockMvc - .perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty") - .param("birthDate", "2015-02-12")) - .andExpect(model().attributeHasNoErrors("owner")) - .andExpect(model().attributeHasErrors("pet")) - .andExpect(model().attributeHasFieldErrors("pet", "type")) - .andExpect(model().attributeHasFieldErrorCode("pet", "type", "required")) - .andExpect(status().isOk()) - .andExpect(view().name("pets/createOrUpdatePetForm")); - } - - @Test - void testInitUpdateForm() throws Exception { - mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID)) - .andExpect(status().isOk()) - .andExpect(model().attributeExists("pet")) - .andExpect(view().name("pets/createOrUpdatePetForm")); - } - - @Test - void testProcessUpdateFormSuccess() throws Exception { - mockMvc - .perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty") - .param("type", "hamster") - .param("birthDate", "2015-02-12")) - .andExpect(status().is3xxRedirection()) - .andExpect(view().name("redirect:/owners/{ownerId}")); - } - - @Test - void testProcessUpdateFormHasErrors() throws Exception { - mockMvc - .perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty") - .param("birthDate", "2015/02/12")) - .andExpect(model().attributeHasNoErrors("owner")) - .andExpect(model().attributeHasErrors("pet")) - .andExpect(status().isOk()) - .andExpect(view().name("pets/createOrUpdatePetForm")); - } - -} diff --git a/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java b/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java deleted file mode 100644 index d7240f351..000000000 --- a/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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. - */ - -package org.springframework.samples.petclinic.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.LocalDate; -import java.util.Collection; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.samples.petclinic.owner.Owner; -import org.springframework.samples.petclinic.owner.OwnerRepository; -import org.springframework.samples.petclinic.owner.Pet; -import org.springframework.samples.petclinic.owner.PetType; -import org.springframework.samples.petclinic.owner.Visit; -import org.springframework.samples.petclinic.vet.Vet; -import org.springframework.samples.petclinic.vet.VetRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * Integration test of the Service and the Repository layer. - *

- * ClinicServiceSpringDataJpaTests subclasses benefit from the following services provided - * by the Spring TestContext Framework: - *

- *
    - *
  • Spring IoC container caching which spares us unnecessary set up - * time between test execution.
  • - *
  • Dependency Injection of test fixture instances, meaning that we - * don't need to perform application context lookups. See the use of - * {@link Autowired @Autowired} on the instance variable, which uses - * autowiring by type. - *
  • Transaction management, meaning each test method is executed in - * its own transaction, which is automatically rolled back by default. Thus, even if tests - * insert or otherwise change database state, there is no need for a teardown or cleanup - * script. - *
  • An {@link org.springframework.context.ApplicationContext ApplicationContext} is - * also inherited and can be used for explicit bean lookup if necessary.
  • - *
- * - * @author Ken Krebs - * @author Rod Johnson - * @author Juergen Hoeller - * @author Sam Brannen - * @author Michael Isvy - * @author Dave Syer - */ -@DataJpaTest(includeFilters = @ComponentScan.Filter(Service.class)) -// Ensure that if the mysql profile is active we connect to the real database: -@AutoConfigureTestDatabase(replace = Replace.NONE) -// @TestPropertySource("/application-postgres.properties") -class ClinicServiceTests { - - @Autowired - protected OwnerRepository owners; - - @Autowired - protected VetRepository vets; - - Pageable pageable; - - @Test - void shouldFindOwnersByLastName() { - Page owners = this.owners.findByLastName("Davis", pageable); - assertThat(owners).hasSize(2); - - owners = this.owners.findByLastName("Daviss", pageable); - assertThat(owners).isEmpty(); - } - - @Test - void shouldFindSingleOwnerWithPet() { - Owner owner = this.owners.findById(1); - assertThat(owner.getLastName()).startsWith("Franklin"); - assertThat(owner.getPets()).hasSize(1); - assertThat(owner.getPets().get(0).getType()).isNotNull(); - assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("cat"); - } - - @Test - @Transactional - void shouldInsertOwner() { - Page owners = this.owners.findByLastName("Schultz", pageable); - int found = (int) owners.getTotalElements(); - - Owner owner = new Owner(); - owner.setFirstName("Sam"); - owner.setLastName("Schultz"); - owner.setAddress("4, Evans Street"); - owner.setCity("Wollongong"); - owner.setTelephone("4444444444"); - this.owners.save(owner); - assertThat(owner.getId().longValue()).isNotEqualTo(0); - - owners = this.owners.findByLastName("Schultz", pageable); - assertThat(owners.getTotalElements()).isEqualTo(found + 1); - } - - @Test - @Transactional - void shouldUpdateOwner() { - Owner owner = this.owners.findById(1); - String oldLastName = owner.getLastName(); - String newLastName = oldLastName + "X"; - - owner.setLastName(newLastName); - this.owners.save(owner); - - // retrieving new name from database - owner = this.owners.findById(1); - assertThat(owner.getLastName()).isEqualTo(newLastName); - } - - @Test - void shouldFindAllPetTypes() { - Collection petTypes = this.owners.findPetTypes(); - - PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1); - assertThat(petType1.getName()).isEqualTo("cat"); - PetType petType4 = EntityUtils.getById(petTypes, PetType.class, 4); - assertThat(petType4.getName()).isEqualTo("snake"); - } - - @Test - @Transactional - void shouldInsertPetIntoDatabaseAndGenerateId() { - Owner owner6 = this.owners.findById(6); - int found = owner6.getPets().size(); - - Pet pet = new Pet(); - pet.setName("bowser"); - Collection types = this.owners.findPetTypes(); - pet.setType(EntityUtils.getById(types, PetType.class, 2)); - pet.setBirthDate(LocalDate.now()); - owner6.addPet(pet); - assertThat(owner6.getPets().size()).isEqualTo(found + 1); - - this.owners.save(owner6); - - owner6 = this.owners.findById(6); - assertThat(owner6.getPets().size()).isEqualTo(found + 1); - // checks that id has been generated - pet = owner6.getPet("bowser"); - assertThat(pet.getId()).isNotNull(); - } - - @Test - @Transactional - void shouldUpdatePetName() throws Exception { - Owner owner6 = this.owners.findById(6); - Pet pet7 = owner6.getPet(7); - String oldName = pet7.getName(); - - String newName = oldName + "X"; - pet7.setName(newName); - this.owners.save(owner6); - - owner6 = this.owners.findById(6); - pet7 = owner6.getPet(7); - assertThat(pet7.getName()).isEqualTo(newName); - } - - @Test - void shouldFindVets() { - Collection vets = this.vets.findAll(); - - Vet vet = EntityUtils.getById(vets, Vet.class, 3); - assertThat(vet.getLastName()).isEqualTo("Douglas"); - assertThat(vet.getNrOfSpecialties()).isEqualTo(2); - assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry"); - assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery"); - } - - @Test - @Transactional - void shouldAddNewVisitForPet() { - Owner owner6 = this.owners.findById(6); - Pet pet7 = owner6.getPet(7); - int found = pet7.getVisits().size(); - Visit visit = new Visit(); - visit.setDescription("test"); - - owner6.addVisit(pet7.getId(), visit); - this.owners.save(owner6); - - owner6 = this.owners.findById(6); - - assertThat(pet7.getVisits()) // - .hasSize(found + 1) // - .allMatch(value -> value.getId() != null); - } - - @Test - void shouldFindVisitsByPetId() throws Exception { - Owner owner6 = this.owners.findById(6); - Pet pet7 = owner6.getPet(7); - Collection visits = pet7.getVisits(); - - assertThat(visits) // - .hasSize(2) // - .element(0) - .extracting(Visit::getDate) - .isNotNull(); - } - -} diff --git a/src/test/jmeter/petclinic_test_plan.jmx b/src/test/jmeter/petclinic_test_plan.jmx index 8014ffbec..b4a6b719e 100644 --- a/src/test/jmeter/petclinic_test_plan.jmx +++ b/src/test/jmeter/petclinic_test_plan.jmx @@ -9,12 +9,12 @@ PETCLINIC_HOST - localhost + a25c892af46df47589102bdef28fda12-1374796034.eu-west-1.elb.amazonaws.com = PETCLINIC_PORT - 8080 + 80 =