From f76d2b073138d07dbaeaf9b54b9ad60bddf37395 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Thu, 24 Aug 2023 18:45:12 +0200 Subject: [PATCH 1/3] feat: Initial setup --- .gitignore | 2 + README.md | 70 ++++++++++++++++++++++ deployment/base/gogs/deployment.yaml | 22 +++++++ deployment/base/gogs/ingress.yaml | 19 ++++++ deployment/base/gogs/kustomization.yaml | 7 +++ deployment/base/gogs/service.yaml | 14 +++++ deployment/base/jenkins/deployment.yaml | 22 +++++++ deployment/base/jenkins/ingress.yaml | 19 ++++++ deployment/base/jenkins/kustomization.yaml | 7 +++ deployment/base/jenkins/service.yaml | 14 +++++ deployment/base/kustomization.yaml | 8 +++ deployment/base/nginx/kustomization.yaml | 5 ++ deployment/base/pact/deployment.yaml | 29 +++++++++ deployment/base/pact/ingress.yaml | 19 ++++++ deployment/base/pact/kustomization.yaml | 7 +++ deployment/base/pact/service.yaml | 14 +++++ deployment/cluster-config/kind-config.yml | 20 +++++++ deployment/kustomization.yaml | 6 ++ deployment/setup/job.yaml | 12 ++++ deployment/setup/kustomization.yaml | 5 ++ docker-compose.yaml | 10 ++++ gogs/Dockerfile | 1 + jenkins/Dockerfile | 1 + setup/Dockerfile | 5 ++ setup/setup.sh | 1 + 25 files changed, 339 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 deployment/base/gogs/deployment.yaml create mode 100644 deployment/base/gogs/ingress.yaml create mode 100644 deployment/base/gogs/kustomization.yaml create mode 100644 deployment/base/gogs/service.yaml create mode 100644 deployment/base/jenkins/deployment.yaml create mode 100644 deployment/base/jenkins/ingress.yaml create mode 100644 deployment/base/jenkins/kustomization.yaml create mode 100644 deployment/base/jenkins/service.yaml create mode 100644 deployment/base/kustomization.yaml create mode 100644 deployment/base/nginx/kustomization.yaml create mode 100644 deployment/base/pact/deployment.yaml create mode 100644 deployment/base/pact/ingress.yaml create mode 100644 deployment/base/pact/kustomization.yaml create mode 100644 deployment/base/pact/service.yaml create mode 100644 deployment/cluster-config/kind-config.yml create mode 100644 deployment/kustomization.yaml create mode 100644 deployment/setup/job.yaml create mode 100644 deployment/setup/kustomization.yaml create mode 100644 docker-compose.yaml create mode 100644 gogs/Dockerfile create mode 100644 jenkins/Dockerfile create mode 100644 setup/Dockerfile create mode 100644 setup/setup.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ea0f13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.* +!.gitignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b3df7b --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Workshop API Design + +Herzlich willkommen zum Workshop API Design. + +## Aufsetzen eines Kubernetes-Clusters mit Minikube + +Wir verwenden Minikube, um einen lokalen Kubernetes-Cluster aufzusetzen. + +Bitte installieren Sie [Minikube](https://kubernetes.io/de/docs/tasks/tools/install-minikube/#minikube-installieren). + +Bitte starten Sie Minikube, indem sie folgenden Befehl ausführen: + +```shell +minikube start --cpus 4 --memory 4096 +``` + +## Jenkins konfigurieren + +Wir möchten einen Jenkins-Server im Cluster betreiben, der in den Cluster deployen kann. +Damit das möglich ist, benötigt der Jenkins-Server die Konfiguration für den Cluster-Zugriff +Bitte legen Sie diese an, indem Sie folgenden Befehl ausführen: + +```shell +kubectl config view --raw > jenkins/kube-config +``` + +## Bauen der Docker-Images und Laden in den Cluster + +```shell +docker compose build +minikube image load host.docker.internal:5000/gogs:local +minikube image load host.docker.internal:5000/jenkins:local +minikube image load host.docker.internal:5000/setup:local +minikube image load host.docker.internal:5000/delivery-db:local +``` + +## Initialisierung des Clusters mit Kustomize + +```shell +kubectl apply -k ./deployment/ +``` + +## Zugriff auf den Cluster + +Wenn der Cluster und die Services gestartet sind (das wird etwas dauern), +können Sie minikube so konfigurieren, dass auf alle Services zugegriffen werden kann: + +```shell +minikube service --all +``` + +Die URLs werden dann in der Konsole ausgegeben. + +Melden Sie sich beim Gogs-Service (Git-Server) mit folgenden Zugangsdaten an: + +``` +username: openknowledge +password: workshop +``` + +Für die anderen Services ist keine Authentifizierung nötig. + +## Entfernen des Clusters + +Um den Cluster zu entfernen, rufen Sie folgenen Befehl auf: + +```shell +minikube stop +minikube delete +``` diff --git a/deployment/base/gogs/deployment.yaml b/deployment/base/gogs/deployment.yaml new file mode 100644 index 0000000..55d6cba --- /dev/null +++ b/deployment/base/gogs/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gogs-deployment + labels: + app: gogs-service +spec: + replicas: 1 + selector: + matchLabels: + app: gogs-service + template: + metadata: + labels: + app: gogs-service + spec: + containers: + - name: gogs + image: host.docker.internal:5000/gogs:local + ports: + - containerPort: 3000 + name: http diff --git a/deployment/base/gogs/ingress.yaml b/deployment/base/gogs/ingress.yaml new file mode 100644 index 0000000..e721a3d --- /dev/null +++ b/deployment/base/gogs/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gogs-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: gogs.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gogs-service + port: + number: 30090 diff --git a/deployment/base/gogs/kustomization.yaml b/deployment/base/gogs/kustomization.yaml new file mode 100644 index 0000000..350ec31 --- /dev/null +++ b/deployment/base/gogs/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + - ingress.yaml \ No newline at end of file diff --git a/deployment/base/gogs/service.yaml b/deployment/base/gogs/service.yaml new file mode 100644 index 0000000..c271e17 --- /dev/null +++ b/deployment/base/gogs/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: gogs-service +spec: + selector: + app: gogs-service + type: NodePort + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + nodePort: 30060 + name: service diff --git a/deployment/base/jenkins/deployment.yaml b/deployment/base/jenkins/deployment.yaml new file mode 100644 index 0000000..e02aff3 --- /dev/null +++ b/deployment/base/jenkins/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jenkins-deployment + labels: + app: jenkins-service +spec: + replicas: 1 + selector: + matchLabels: + app: jenkins-service + template: + metadata: + labels: + app: jenkins-service + spec: + containers: + - name: jenkins + image: host.docker.internal:5000/jenkins:local + ports: + - containerPort: 8080 + name: http diff --git a/deployment/base/jenkins/ingress.yaml b/deployment/base/jenkins/ingress.yaml new file mode 100644 index 0000000..25703f4 --- /dev/null +++ b/deployment/base/jenkins/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: jenkins-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: jenkins.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: jenkins-service + port: + number: 30090 diff --git a/deployment/base/jenkins/kustomization.yaml b/deployment/base/jenkins/kustomization.yaml new file mode 100644 index 0000000..350ec31 --- /dev/null +++ b/deployment/base/jenkins/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + - ingress.yaml \ No newline at end of file diff --git a/deployment/base/jenkins/service.yaml b/deployment/base/jenkins/service.yaml new file mode 100644 index 0000000..1889019 --- /dev/null +++ b/deployment/base/jenkins/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: jenkins-service +spec: + selector: + app: jenkins-service + type: NodePort + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + nodePort: 30070 + name: service diff --git a/deployment/base/kustomization.yaml b/deployment/base/kustomization.yaml new file mode 100644 index 0000000..d16774b --- /dev/null +++ b/deployment/base/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ./nginx + - ./pact + - ./gogs + - ./jenkins diff --git a/deployment/base/nginx/kustomization.yaml b/deployment/base/nginx/kustomization.yaml new file mode 100644 index 0000000..d603cab --- /dev/null +++ b/deployment/base/nginx/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml diff --git a/deployment/base/pact/deployment.yaml b/deployment/base/pact/deployment.yaml new file mode 100644 index 0000000..4e1b23f --- /dev/null +++ b/deployment/base/pact/deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pact-deployment + labels: + app: pact-service +spec: + replicas: 1 + selector: + matchLabels: + app: pact-service + template: + metadata: + labels: + app: pact-service + spec: + containers: + - name: pact + image: pactfoundation/pact-broker:2.111.0-pactbroker2.107.1 + ports: + - containerPort: 8080 + name: http + env: + - name: PACT_BROKER_PORT + value: "8080" + - name: PACT_BROKER_DATABASE_ADAPTER + value: sqlite + - name: PACT_BROKER_DATABASE_NAME + value: pact_broker.sqlite diff --git a/deployment/base/pact/ingress.yaml b/deployment/base/pact/ingress.yaml new file mode 100644 index 0000000..100e93b --- /dev/null +++ b/deployment/base/pact/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pact-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: pact.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: pact-service + port: + number: 30080 diff --git a/deployment/base/pact/kustomization.yaml b/deployment/base/pact/kustomization.yaml new file mode 100644 index 0000000..350ec31 --- /dev/null +++ b/deployment/base/pact/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + - ingress.yaml \ No newline at end of file diff --git a/deployment/base/pact/service.yaml b/deployment/base/pact/service.yaml new file mode 100644 index 0000000..8011500 --- /dev/null +++ b/deployment/base/pact/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: pact-service +spec: + selector: + app: pact-service + type: NodePort + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + nodePort: 30050 + name: service diff --git a/deployment/cluster-config/kind-config.yml b/deployment/cluster-config/kind-config.yml new file mode 100644 index 0000000..8ce9c1d --- /dev/null +++ b/deployment/cluster-config/kind-config.yml @@ -0,0 +1,20 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + extraPortMappings: + - containerPort: 30050 + hostPort: 30050 + protocol: TCP + - containerPort: 30060 + hostPort: 30060 + protocol: TCP + - containerPort: 30070 + hostPort: 30070 + protocol: TCP diff --git a/deployment/kustomization.yaml b/deployment/kustomization.yaml new file mode 100644 index 0000000..8ebfae3 --- /dev/null +++ b/deployment/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ./base + - ./setup diff --git a/deployment/setup/job.yaml b/deployment/setup/job.yaml new file mode 100644 index 0000000..118bbeb --- /dev/null +++ b/deployment/setup/job.yaml @@ -0,0 +1,12 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: setup +spec: + template: + spec: + containers: + - name: setup + image: host.docker.internal:5000/setup:local + restartPolicy: Never + backoffLimit: 1 diff --git a/deployment/setup/kustomization.yaml b/deployment/setup/kustomization.yaml new file mode 100644 index 0000000..3e6455e --- /dev/null +++ b/deployment/setup/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - job.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b4ae4ae --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + gogs: + build: gogs/ + image: host.docker.internal:5000/gogs:local + jenkins: + build: jenkins/ + image: host.docker.internal:5000/jenkins:local + setup: + build: setup/ + image: host.docker.internal:5000/setup:local diff --git a/gogs/Dockerfile b/gogs/Dockerfile new file mode 100644 index 0000000..88a38c2 --- /dev/null +++ b/gogs/Dockerfile @@ -0,0 +1 @@ +FROM gogs/gogs:0.13 \ No newline at end of file diff --git a/jenkins/Dockerfile b/jenkins/Dockerfile new file mode 100644 index 0000000..a6e29fc --- /dev/null +++ b/jenkins/Dockerfile @@ -0,0 +1 @@ +FROM jenkins/jenkins:2.414.1-lts-jdk11 diff --git a/setup/Dockerfile b/setup/Dockerfile new file mode 100644 index 0000000..a205006 --- /dev/null +++ b/setup/Dockerfile @@ -0,0 +1,5 @@ +FROM bash:5.2.15 + +COPY setup.sh / + +CMD ["bash", "/setup.sh"] diff --git a/setup/setup.sh b/setup/setup.sh new file mode 100644 index 0000000..81f2cc8 --- /dev/null +++ b/setup/setup.sh @@ -0,0 +1 @@ +echo "Hello Setup" From d1155d01f90258decfc236c337a0a03ba01f5e5b Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Mon, 28 Aug 2023 20:31:46 +0200 Subject: [PATCH 2/3] feat: Add consumer driven contract testing --- .gitignore | 4 + ...r-Driven Contracts.postman_collection.json | 328 + apps/address-validation-service/.gitignore | 1 + apps/address-validation-service/Dockerfile | 7 + apps/address-validation-service/Jenkinsfile | 125 + apps/address-validation-service/README.md | 3 + .../deployment/base/deployment.yaml | 22 + .../deployment/base/kustomization.yaml | 6 + .../deployment/base/service.yaml | 14 + .../overlays/prod/kustomization.yaml | 19 + .../overlays/prod/patches/port-patch.yaml | 14 + .../overlays/test/kustomization.yaml | 12 + apps/address-validation-service/pom.xml | 94 + .../application/AddressApplication.java | 26 + .../address/application/AddressResource.java | 92 + .../sample/address/domain/Address.java | 37 + .../address/domain/AddressRepository.java | 67 + .../sample/address/domain/City.java | 105 + .../sample/address/domain/CityName.java | 72 + .../sample/address/domain/HouseNumber.java | 80 + .../sample/address/domain/Street.java | 74 + .../sample/address/domain/StreetName.java | 77 + .../sample/address/domain/ZipCode.java | 76 + .../address/infrastructure/CORSFilter.java | 40 + .../src/main/resources/plz.txt | 8219 +++++++++++++++++ .../address/AddressValidationServiceTest.java | 55 + ...ry-service-address-validation-service.json | 126 + .../sample/address/ambiguous-address.json | 7 + .../sample/address/missing-address.json | 7 + .../sample/address/misspelled-address.json | 7 + .../sample/address/valid-address.json | 7 + apps/billing-service/.gitignore | 1 + apps/billing-service/Dockerfile | 7 + apps/billing-service/Jenkinsfile | 125 + apps/billing-service/README.md | 3 + .../deployment/base/deployment.yaml | 22 + .../deployment/base/kustomization.yaml | 6 + .../deployment/base/service.yaml | 14 + .../overlays/prod/kustomization.yaml | 19 + .../overlays/prod/patches/port-patch.yaml | 14 + .../overlays/test/kustomization.yaml | 12 + apps/billing-service/pom.xml | 94 + .../application/AddressApplication.java | 26 + .../address/application/AddressResource.java | 69 + .../sample/address/domain/Address.java | 62 + .../sample/address/domain/AddressLine.java | 115 + .../address/domain/AddressRepository.java | 58 + .../sample/address/domain/City.java | 118 + .../sample/address/domain/CityName.java | 72 + .../sample/address/domain/CustomerNumber.java | 76 + .../sample/address/domain/HouseNumber.java | 80 + .../sample/address/domain/Location.java | 66 + .../sample/address/domain/Recipient.java | 82 + .../sample/address/domain/Street.java | 77 + .../sample/address/domain/StreetName.java | 77 + .../sample/address/domain/ZipCode.java | 76 + .../address/BillingAddressServiceTest.java | 56 + .../sample/address/JsonObjectComparision.java | 41 + .../address/domain/TestAddressRepository.java | 25 + .../customer-service-billing-service.json | 111 + .../de/openknowledge/sample/address/007.json | 8 + .../sample/address/0815-new.json | 8 + .../de/openknowledge/sample/address/0815.json | 8 + .../de/openknowledge/sample/address/0816.json | 8 + apps/customer-service/.gitignore | 1 + apps/customer-service/Dockerfile | 7 + apps/customer-service/Jenkinsfile | 125 + apps/customer-service/README.md | 3 + .../deployment/base/deployment.yaml | 27 + .../deployment/base/kustomization.yaml | 6 + .../deployment/base/service.yaml | 14 + .../overlays/prod/kustomization.yaml | 19 + .../overlays/prod/patches/port-patch.yaml | 14 + .../overlays/test/kustomization.yaml | 12 + apps/customer-service/pom.xml | 133 + .../sample/address/domain/Address.java | 107 + .../sample/address/domain/AddressLine.java | 115 + .../domain/BillingAddressRepository.java | 76 + .../sample/address/domain/City.java | 113 + .../sample/address/domain/CityName.java | 72 + .../domain/DeliveryAddressRepository.java | 91 + .../sample/address/domain/HouseNumber.java | 80 + .../sample/address/domain/Recipient.java | 82 + .../sample/address/domain/Street.java | 75 + .../sample/address/domain/StreetName.java | 77 + .../sample/address/domain/ZipCode.java | 76 + .../address/infrastructure/CORSFilter.java | 40 + .../ValidationExceptionHandler.java | 32 + .../application/CustomerApplication.java | 26 + .../application/CustomerResource.java | 113 + .../sample/customer/domain/Customer.java | 99 + .../sample/customer/domain/CustomerName.java | 77 + .../customer/domain/CustomerNumber.java | 76 + .../customer/domain/CustomerRepository.java | 67 + .../META-INF/microprofile-config.properties | 2 + .../domain/BillingAddressRepositoryTest.java | 130 + .../domain/DeliveryAddressRepositoryTest.java | 166 + .../sample/customer/CustomerServiceTest.java | 210 + .../customer/JsonObjectComparision.java | 41 + .../sample/infrastructure/CdiMock.java | 35 + .../src/test/resources/META-INF/beans.xml | 11 + .../sample/customer/erika-with-addresses.json | 12 + .../openknowledge/sample/customer/erika.json | 4 + .../sample/customer/james-with-addresses.json | 12 + .../openknowledge/sample/customer/james.json | 4 + .../sample/customer/max-with-addresses.json | 20 + .../de/openknowledge/sample/customer/max.json | 4 + .../sample/customer/sherlock-address.json | 8 + .../sample/customer/sherlock.json | 3 + apps/delivery-service/.gitignore | 1 + apps/delivery-service/Dockerfile | 8 + apps/delivery-service/Jenkinsfile | 125 + apps/delivery-service/README.md | 18 + .../deployment/base/deployment.yaml | 52 + .../deployment/base/kustomization.yaml | 6 + .../deployment/base/service.yaml | 28 + .../overlays/prod/kustomization.yaml | 19 + .../overlays/prod/patches/port-patch.yaml | 14 + .../overlays/test/kustomization.yaml | 12 + apps/delivery-service/namespaces.yaml | 13 + apps/delivery-service/pom.xml | 173 + .../application/AddressesApplication.java | 26 + .../application/AddressesResource.java | 73 + .../sample/address/domain/Address.java | 82 + .../sample/address/domain/AddressLine.java | 115 + .../domain/AddressValidationService.java | 44 + .../address/domain/AddressesRepository.java | 45 + .../sample/address/domain/City.java | 121 + .../sample/address/domain/CityName.java | 76 + .../sample/address/domain/CustomerNumber.java | 83 + .../sample/address/domain/HouseNumber.java | 82 + .../sample/address/domain/Location.java | 70 + .../sample/address/domain/Recipient.java | 86 + .../sample/address/domain/Street.java | 84 + .../sample/address/domain/StreetName.java | 79 + .../sample/address/domain/ZipCode.java | 80 + .../address/infrastructure/CORSFilter.java | 40 + .../ValidationExceptionHandler.java | 34 + .../META-INF/microprofile-config.properties | 1 + .../main/resources/META-INF/persistence.xml | 15 + .../src/main/resources/sql/create.sql | 1 + .../address/DeliveryAddressServiceTest.java | 97 + .../sample/address/JsonObjectComparision.java | 41 + .../domain/AddressValidationServiceTest.java | 112 + .../sample/infrastructure/CdiMock.java | 35 + .../infrastructure/H2DatabaseCleanup.java | 41 + .../sample/infrastructure/ScriptExecutor.java | 43 + .../customer-service-delivery-service.json | 180 + .../src/test/resources/c3p0.properties | 1 + .../sample/address/007-invalid.json | 8 + .../sample/address/0815-new.json | 8 + .../de/openknowledge/sample/address/0815.json | 8 + .../sample/address/0816-invalid.json | 8 + .../de/openknowledge/sample/address/0816.json | 8 + .../de/openknowledge/sample/address/0817.json | 8 + .../src/test/resources/h2-ds.xml | 20 + .../src/test/resources/sql/data.sql | 2 + delivery-db/Dockerfile | 2 + deployment/base/gogs/deployment.yaml | 25 + deployment/base/gogs/ingress.yaml | 2 +- deployment/base/gogs/service.yaml | 16 +- deployment/base/jenkins/deployment.yaml | 7 + deployment/base/jenkins/ingress.yaml | 2 +- deployment/base/jenkins/service.yaml | 2 +- deployment/base/kustomization.yaml | 2 + deployment/base/namespaces.yaml | 13 + deployment/base/pact/deployment.yaml | 6 + deployment/base/pact/ingress.yaml | 2 +- deployment/base/pact/service.yaml | 2 +- deployment/base/registry/deployment.yaml | 22 + deployment/base/registry/ingress.yaml | 19 + deployment/base/registry/kustomization.yaml | 7 + deployment/base/registry/service.yaml | 14 + deployment/cluster-config/kind-config.yml | 42 +- docker-compose.yaml | 9 +- gogs/Dockerfile | 3 +- gogs/app.ini | 15 + jenkins/Dockerfile | 38 + jenkins/executors.groovy | 2 + scripts/.gitignore | 2 + scripts/cleanJobs.groovy | 9 + scripts/cleanJobs.sh | 7 + scripts/jenkinsCurl.sh | 14 + scripts/push.sh | 36 + scripts/pushAll.sh | 11 + setup/Dockerfile | 10 +- .../address-validation-service/config.xml | 58 + setup/jenkins/billing-service/config.xml | 58 + setup/jenkins/create-jobs.sh | 28 + setup/jenkins/customer-service/config.xml | 58 + setup/jenkins/delivery-service/config.xml | 58 + setup/pushToGogs.sh | 35 + setup/setup.sh | 51 +- 193 files changed, 16975 insertions(+), 14 deletions(-) create mode 100644 Consumer-Driven Contracts.postman_collection.json create mode 100644 apps/address-validation-service/.gitignore create mode 100644 apps/address-validation-service/Dockerfile create mode 100644 apps/address-validation-service/Jenkinsfile create mode 100644 apps/address-validation-service/README.md create mode 100644 apps/address-validation-service/deployment/base/deployment.yaml create mode 100644 apps/address-validation-service/deployment/base/kustomization.yaml create mode 100644 apps/address-validation-service/deployment/base/service.yaml create mode 100644 apps/address-validation-service/deployment/overlays/prod/kustomization.yaml create mode 100644 apps/address-validation-service/deployment/overlays/prod/patches/port-patch.yaml create mode 100644 apps/address-validation-service/deployment/overlays/test/kustomization.yaml create mode 100644 apps/address-validation-service/pom.xml create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 apps/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java create mode 100644 apps/address-validation-service/src/main/resources/plz.txt create mode 100644 apps/address-validation-service/src/test/java/de/openknowledge/sample/address/AddressValidationServiceTest.java create mode 100644 apps/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json create mode 100644 apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json create mode 100644 apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json create mode 100644 apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json create mode 100644 apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json create mode 100644 apps/billing-service/.gitignore create mode 100644 apps/billing-service/Dockerfile create mode 100644 apps/billing-service/Jenkinsfile create mode 100644 apps/billing-service/README.md create mode 100644 apps/billing-service/deployment/base/deployment.yaml create mode 100644 apps/billing-service/deployment/base/kustomization.yaml create mode 100644 apps/billing-service/deployment/base/service.yaml create mode 100644 apps/billing-service/deployment/overlays/prod/kustomization.yaml create mode 100644 apps/billing-service/deployment/overlays/prod/patches/port-patch.yaml create mode 100644 apps/billing-service/deployment/overlays/test/kustomization.yaml create mode 100644 apps/billing-service/pom.xml create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 apps/billing-service/src/test/java/de/openknowledge/sample/address/BillingAddressServiceTest.java create mode 100644 apps/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java create mode 100644 apps/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java create mode 100644 apps/billing-service/src/test/pacts/customer-service-billing-service.json create mode 100644 apps/billing-service/src/test/resources/de/openknowledge/sample/address/007.json create mode 100644 apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json create mode 100644 apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json create mode 100644 apps/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json create mode 100644 apps/customer-service/.gitignore create mode 100644 apps/customer-service/Dockerfile create mode 100644 apps/customer-service/Jenkinsfile create mode 100644 apps/customer-service/README.md create mode 100644 apps/customer-service/deployment/base/deployment.yaml create mode 100644 apps/customer-service/deployment/base/kustomization.yaml create mode 100644 apps/customer-service/deployment/base/service.yaml create mode 100644 apps/customer-service/deployment/overlays/prod/kustomization.yaml create mode 100644 apps/customer-service/deployment/overlays/prod/patches/port-patch.yaml create mode 100644 apps/customer-service/deployment/overlays/test/kustomization.yaml create mode 100644 apps/customer-service/pom.xml create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java create mode 100644 apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java create mode 100644 apps/customer-service/src/main/resources/META-INF/microprofile-config.properties create mode 100644 apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/BillingAddressRepositoryTest.java create mode 100644 apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/DeliveryAddressRepositoryTest.java create mode 100644 apps/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java create mode 100644 apps/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java create mode 100644 apps/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java create mode 100644 apps/customer-service/src/test/resources/META-INF/beans.xml create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json create mode 100644 apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json create mode 100644 apps/delivery-service/.gitignore create mode 100644 apps/delivery-service/Dockerfile create mode 100644 apps/delivery-service/Jenkinsfile create mode 100644 apps/delivery-service/README.md create mode 100644 apps/delivery-service/deployment/base/deployment.yaml create mode 100644 apps/delivery-service/deployment/base/kustomization.yaml create mode 100644 apps/delivery-service/deployment/base/service.yaml create mode 100644 apps/delivery-service/deployment/overlays/prod/kustomization.yaml create mode 100644 apps/delivery-service/deployment/overlays/prod/patches/port-patch.yaml create mode 100644 apps/delivery-service/deployment/overlays/test/kustomization.yaml create mode 100644 apps/delivery-service/namespaces.yaml create mode 100644 apps/delivery-service/pom.xml create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressesRepository.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java create mode 100644 apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java create mode 100644 apps/delivery-service/src/main/resources/META-INF/microprofile-config.properties create mode 100644 apps/delivery-service/src/main/resources/META-INF/persistence.xml create mode 100644 apps/delivery-service/src/main/resources/sql/create.sql create mode 100644 apps/delivery-service/src/test/java/de/openknowledge/sample/address/DeliveryAddressServiceTest.java create mode 100644 apps/delivery-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java create mode 100644 apps/delivery-service/src/test/java/de/openknowledge/sample/address/domain/AddressValidationServiceTest.java create mode 100644 apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java create mode 100644 apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/H2DatabaseCleanup.java create mode 100644 apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/ScriptExecutor.java create mode 100644 apps/delivery-service/src/test/pacts/customer-service-delivery-service.json create mode 100644 apps/delivery-service/src/test/resources/c3p0.properties create mode 100644 apps/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json create mode 100644 apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json create mode 100644 apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json create mode 100644 apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json create mode 100644 apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json create mode 100644 apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json create mode 100644 apps/delivery-service/src/test/resources/h2-ds.xml create mode 100644 apps/delivery-service/src/test/resources/sql/data.sql create mode 100644 delivery-db/Dockerfile create mode 100644 deployment/base/namespaces.yaml create mode 100644 deployment/base/registry/deployment.yaml create mode 100644 deployment/base/registry/ingress.yaml create mode 100644 deployment/base/registry/kustomization.yaml create mode 100644 deployment/base/registry/service.yaml create mode 100644 gogs/app.ini create mode 100644 jenkins/executors.groovy create mode 100644 scripts/.gitignore create mode 100644 scripts/cleanJobs.groovy create mode 100755 scripts/cleanJobs.sh create mode 100755 scripts/jenkinsCurl.sh create mode 100755 scripts/push.sh create mode 100755 scripts/pushAll.sh create mode 100644 setup/jenkins/address-validation-service/config.xml create mode 100644 setup/jenkins/billing-service/config.xml create mode 100755 setup/jenkins/create-jobs.sh create mode 100644 setup/jenkins/customer-service/config.xml create mode 100644 setup/jenkins/delivery-service/config.xml create mode 100755 setup/pushToGogs.sh mode change 100644 => 100755 setup/setup.sh diff --git a/.gitignore b/.gitignore index 9ea0f13..d9a8b25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .* !.gitignore + +*.iml + +jenkins/kube-config diff --git a/Consumer-Driven Contracts.postman_collection.json b/Consumer-Driven Contracts.postman_collection.json new file mode 100644 index 0000000..279beea --- /dev/null +++ b/Consumer-Driven Contracts.postman_collection.json @@ -0,0 +1,328 @@ +{ + "info": { + "_postman_id": "38c24193-20db-400c-894c-93e289835152", + "name": "Consumer-Driven Contracts", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "27875178" + }, + "item": [ + { + "name": "Get Customers", + "request": { + "method": "GET", + "header": [], + "url": "localhost:30000/customers" + }, + "response": [] + }, + { + "name": "Get Customers (test)", + "request": { + "method": "GET", + "header": [], + "url": "localhost:31000/customers" + }, + "response": [] + }, + { + "name": "Get single Customer", + "request": { + "method": "GET", + "header": [], + "url": "localhost:30000/customers/007" + }, + "response": [] + }, + { + "name": "Get single Customer (test)", + "request": { + "method": "GET", + "header": [], + "url": "localhost:31000/customers/007" + }, + "response": [] + }, + { + "name": "Set Billing Address", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"recipient\": \"Sherlock Holmes\",\n \"street\": {\n \"name\": \"Baker Street\",\n \"number\": \"221B\"\n },\n \"city\": \"London NW1 6XE\"\n}\n" + }, + "url": "localhost:30000/customers/007/billing-address" + }, + "response": [] + }, + { + "name": "Set Billing Address (test)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"recipient\": \"James Bond\",\n \"street\": {\n \"name\": \"Baker Street\",\n \"number\": \"221B\"\n },\n \"city\": \"London NW1 6XE\"\n}\n" + }, + "url": "localhost:31000/customers/007/billing-address" + }, + "response": [] + }, + { + "name": "Set Delivery Address", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"recipient\": \"Erika Mustermann\",\n \"street\": {\n \"name\": \"Test\",\n \"number\": \"1\"\n },\n \"city\": \"26122 Oldenburg\"\n}\n" + }, + "url": "localhost:30000/customers/007/delivery-address" + }, + "response": [] + }, + { + "name": "Set Delivery Address (test)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"recipient\": \"Max Mustermann\",\n \"street\": {\n \"name\": \"Test\",\n \"number\": \"1\"\n },\n \"city\": \"26122 Oldenburg\"\n}\n" + }, + "url": "localhost:31000/customers/007/delivery-address" + }, + "response": [] + }, + { + "name": "Validate Address (test)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"recipient\": \"Max Mustermann\",\n \"street\": {\n \"name\": \"Poststrasse\",\n \"number\": \"1\"\n },\n \"city\": \"26122 Oldenburg\"\n}\n" + }, + "url": "localhost:31003/valid-addresses" + }, + "response": [] + }, + { + "name": "Get Billing Address (test)", + "request": { + "method": "GET", + "header": [], + "url": "localhost:31001/billing-addresses/007" + }, + "response": [] + }, + { + "name": "Get Delivery Address (test)", + "request": { + "method": "GET", + "header": [], + "url": "http://localhost:31002/delivery-addresses/007" + }, + "response": [] + }, + { + "name": "Create Webhook for billing-service", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"provider\": {\n \"name\": \"billing-service\"\n },\n \"events\": [{\n \"name\": \"contract_content_changed\"\n }],\n \"request\": {\n \"method\": \"GET\",\n \"url\": \"http://jenkins-service:8080/generic-webhook-trigger/invoke?token=billing-service&stage=${pactbroker.consumerVersionTags}&verifyPacts=true\",\n \"headers\": {\n }\n }\n}" + }, + "url": "http://localhost:30050/webhooks" + }, + "response": [] + }, + { + "name": "Create Webhook for delivery-service", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"provider\": {\n \"name\": \"delivery-service\"\n },\n \"events\": [{\n \"name\": \"contract_content_changed\"\n }],\n \"request\": {\n \"method\": \"GET\",\n \"url\": \"http://jenkins-service:8080/generic-webhook-trigger/invoke?token=delivery-service&stage=${pactbroker.consumerVersionTags}&verifyPacts=true\",\n \"headers\": {\n }\n }\n}" + }, + "url": "http://localhost:30050/webhooks" + }, + "response": [] + }, + { + "name": "Create Webhook for address-validation-service", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"provider\": {\n \"name\": \"address-validation-service\"\n },\n \"events\": [{\n \"name\": \"contract_content_changed\"\n }],\n \"request\": {\n \"method\": \"GET\",\n \"url\": \"http://jenkins-service:8080/generic-webhook-trigger/invoke?token=address-validation-service&stage=${pactbroker.consumerVersionTags}&verifyPacts=true\",\n \"headers\": {\n }\n }\n}" + }, + "url": "http://localhost:30050/webhooks" + }, + "response": [] + }, + { + "name": "Create Verification Webhook for customer-service", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"consumer\": {\n \"name\": \"customer-service\"\n },\n \"events\": [{\n \"name\": \"provider_verification_published\"\n }],\n \"request\": {\n \"method\": \"GET\",\n \"url\": \"http://jenkins-service:8080/generic-webhook-trigger/invoke?token=customer-service&stage=${pactbroker.consumerVersionTags}&deployOnly=true&deploymentVersion=${pactbroker.consumerVersionNumber}\",\n \"headers\": {\n }\n }\n}" + }, + "url": "http://localhost:30050/webhooks" + }, + "response": [] + }, + { + "name": "Create Verification Webhook for delivery-service", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"consumer\": {\n \"name\": \"delivery-service\"\n },\n \"events\": [{\n \"name\": \"provider_verification_published\"\n }],\n \"request\": {\n \"method\": \"GET\",\n \"url\": \"http://jenkins-service:8080/generic-webhook-trigger/invoke?token=delivery-service&stage=${pactbroker.consumerVersionTags}&deployOnly=true&deploymentVersion=${pactbroker.consumerVersionNumber}\",\n \"headers\": {\n }\n }\n}" + }, + "url": "http://localhost:30050/webhooks" + }, + "response": [] + }, + { + "name": "Execute Verification Webhook for Customer Service", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/x-www-form-urlencoded", + "type": "text" + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [] + }, + "url": { + "raw": "http://localhost:9080/generic-webhook-trigger/invoke?token=customer-service&stage=pending-prod&deployOnly=true&deploymentVersion=1.2.1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "9080", + "path": [ + "generic-webhook-trigger", + "invoke" + ], + "query": [ + { + "key": "token", + "value": "customer-service" + }, + { + "key": "stage", + "value": "pending-prod" + }, + { + "key": "deployOnly", + "value": "true" + }, + { + "key": "deploymentVersion", + "value": "1.2.1" + } + ] + } + }, + "response": [] + }, + { + "name": "Set prod tag to Delivery Service", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": "http://localhost:30050/pacticipants/delivery-service/versions/1.2.0/tags/prod" + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/apps/address-validation-service/.gitignore b/apps/address-validation-service/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/apps/address-validation-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/apps/address-validation-service/Dockerfile b/apps/address-validation-service/Dockerfile new file mode 100644 index 0000000..6ab2326 --- /dev/null +++ b/apps/address-validation-service/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:11-jre + +RUN wget https://repo.maven.apache.org/maven2/org/apache/meecrowave/meecrowave-core/1.2.13/meecrowave-core-1.2.13-runner.jar -O /opt/meecrowave-core-runner.jar +ADD target/address-validation-service.war /opt/address-validation-service.war + +EXPOSE 4003 +ENTRYPOINT ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "/opt/meecrowave-core-runner.jar", "--host", "0.0.0.0", "--http", "4003", "--webapp", "/opt/address-validation-service.war"] diff --git a/apps/address-validation-service/Jenkinsfile b/apps/address-validation-service/Jenkinsfile new file mode 100644 index 0000000..4af7ff8 --- /dev/null +++ b/apps/address-validation-service/Jenkinsfile @@ -0,0 +1,125 @@ +#!/usr/bin/env groovy +pipeline { + agent any + + options { + disableConcurrentBuilds() + } + + environment { + SNAPSHOT_VERSION = readMavenPom().getVersion() + LAST_COMMIT_MESSAGE = "${currentBuild.changeSets.size() == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items[currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length - 1].msg}" + PERFORM_RELEASE = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') && env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ')}" + RELEASE_VERSION = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') ? env.SNAPSHOT_VERSION.substring(0, env.SNAPSHOT_VERSION.lastIndexOf('-SNAPSHOT')) : SNAPSHOT_VERSION}" + VERSION = "${env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ') ? env.RELEASE_VERSION : env.SNAPSHOT_VERSION}" + } + + triggers { + pollSCM("* * * * *") + } + + stages { + stage ('Compile') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + echo "Building version ${env.VERSION}" + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn versions:set -DnewVersion=${env.RELEASE_VERSION} -B" + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.RELEASE_VERSION}/g' deployment/overlays/prod/kustomization.yaml" + } else { + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.GIT_COMMIT}/g' deployment/overlays/test/kustomization.yaml" + } + } + sh 'mvn clean test-compile -B' + } + } + stage ('Test') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh "mvn test -B" + } + } + stage ('Package') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh 'mvn package -DskipTests -B' + sh 'docker build -t address-validation .' + } + } + stage ('Push') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh """ + docker tag address-validation localhost:30010/address-validation:${env.VERSION} + docker tag address-validation localhost:30010/address-validation:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker tag address-validation localhost:30010/address-validation:${env.GIT_COMMIT} + docker push localhost:30010/address-validation:${env.VERSION} + docker push localhost:30010/address-validation:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker push localhost:30010/address-validation:${env.GIT_COMMIT} + """ + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh 'git config --global user.name "Jenkins"' + sh 'git config --global user.email "ci@openknowledge.de"' + sh "mvn scm:checkin -Dmessage='release of version ${env.RELEASE_VERSION}' -B" + sh "mvn scm:tag -Dtag=${env.RELEASE_VERSION} -B" + int nextRevision = Integer.parseInt(env.RELEASE_VERSION.substring(env.RELEASE_VERSION.lastIndexOf(".") + 1)) + 1 + nextVersion = RELEASE_VERSION.substring(0, env.RELEASE_VERSION.lastIndexOf(".")) + "." + nextRevision + "-SNAPSHOT" + sh "sed -i 's/${env.RELEASE_VERSION}/${nextVersion}/g' deployment/overlays/prod/kustomization.yaml" + sh "mvn versions:set scm:checkin -DnewVersion=${nextVersion} -Dmessage='update version to ${nextVersion}' -B" + } + } + } + } + stage ('Deploy') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn scm:checkout -DscmVersion=${env.RELEASE_VERSION} -DscmVersionType=tag -B" + sh 'kubectl apply -k deployment/overlays/prod' + } else { + sh 'kubectl apply -k deployment/overlays/test' + sh "sed -i 's/${env.GIT_COMMIT}/${env.SNAPSHOT_VERSION}/g' deployment/overlays/test/kustomization.yaml" + } + } + } + } + } +} diff --git a/apps/address-validation-service/README.md b/apps/address-validation-service/README.md new file mode 100644 index 0000000..2108dca --- /dev/null +++ b/apps/address-validation-service/README.md @@ -0,0 +1,3 @@ +# cdc-address-validation-service + +Address Validation Service to show Consumer-Driven Contracts diff --git a/apps/address-validation-service/deployment/base/deployment.yaml b/apps/address-validation-service/deployment/base/deployment.yaml new file mode 100644 index 0000000..eff9fc5 --- /dev/null +++ b/apps/address-validation-service/deployment/base/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: address-validation-deployment + labels: + app: address-validation-service +spec: + replicas: 1 + selector: + matchLabels: + app: address-validation-service + template: + metadata: + labels: + app: address-validation-service + spec: + containers: + - name: address-validation + image: address-validation:latest + ports: + - containerPort: 4003 + name: http diff --git a/apps/address-validation-service/deployment/base/kustomization.yaml b/apps/address-validation-service/deployment/base/kustomization.yaml new file mode 100644 index 0000000..5b98e94 --- /dev/null +++ b/apps/address-validation-service/deployment/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml diff --git a/apps/address-validation-service/deployment/base/service.yaml b/apps/address-validation-service/deployment/base/service.yaml new file mode 100644 index 0000000..7c9d73f --- /dev/null +++ b/apps/address-validation-service/deployment/base/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: address-validation-service +spec: + selector: + app: address-validation-service + type: NodePort + ports: + - protocol: TCP + port: 4003 + targetPort: 4003 + nodePort: 30090 + name: service diff --git a/apps/address-validation-service/deployment/overlays/prod/kustomization.yaml b/apps/address-validation-service/deployment/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..9a3bb31 --- /dev/null +++ b/apps/address-validation-service/deployment/overlays/prod/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: prod + +resources: + - ../../base + +patches: + - target: + version: v1 + kind: Service + name: address-validation-service + path: ./patches/port-patch.yaml + +images: + - name: address-validation + newName: localhost:30010/address-validation + newTag: 1.1.0-SNAPSHOT diff --git a/apps/address-validation-service/deployment/overlays/prod/patches/port-patch.yaml b/apps/address-validation-service/deployment/overlays/prod/patches/port-patch.yaml new file mode 100644 index 0000000..9ff8142 --- /dev/null +++ b/apps/address-validation-service/deployment/overlays/prod/patches/port-patch.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: address-validation-service +spec: + selector: + app: address-validation-service + type: NodePort + ports: + - protocol: TCP + port: 4003 + targetPort: 4003 + nodePort: 31090 + name: service diff --git a/apps/address-validation-service/deployment/overlays/test/kustomization.yaml b/apps/address-validation-service/deployment/overlays/test/kustomization.yaml new file mode 100644 index 0000000..6879da0 --- /dev/null +++ b/apps/address-validation-service/deployment/overlays/test/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: test + +resources: + - ../../base + +images: + - name: address-validation + newName: localhost:30010/address-validation + newTag: 1.1.0-SNAPSHOT diff --git a/apps/address-validation-service/pom.xml b/apps/address-validation-service/pom.xml new file mode 100644 index 0000000..d38dd52 --- /dev/null +++ b/apps/address-validation-service/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + de.openkonwledge.sample.shop + address-validation-service + 1.1.0-SNAPSHOT + Microservices – Service "Address Validation" + war + + + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/address-validation-service.git + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/address-validation-service.git + + + + 11 + 11 + false + UTF-8 + 1.2.13 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.commons + commons-lang3 + 3.9 + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + au.com.dius.pact.provider + junit5 + 4.3.5 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + + + address-validation-service + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + ${project.version} + + + + + + diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java new file mode 100644 index 0000000..8830a59 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class AddressApplication extends Application { +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java new file mode 100644 index 0000000..b96a4ad --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import static java.util.stream.Collectors.joining; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.AddressRepository; +import de.openknowledge.sample.address.domain.City; + +/** + * RESTFul endpoint for valid addresses + */ +@ApplicationScoped +@Path("/valid-addresses") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AddressResource { + + private static final Logger LOGGER = Logger.getLogger(AddressResource.class.getSimpleName()); + private static final String PROBLEM_JSON_TYPE = "application/problem+json"; + private static final String PROBLEM_JSON = "{\"type\": \"%s\", \"title\": \"%s\", \"status\": %d, \"detail\": \"%s\", \"instance\": \"%s\"}"; + + @Inject + private AddressRepository addressesRepository; + + @POST + @Path("/") + @Consumes(MediaType.APPLICATION_JSON) + public Response validateAddress(Address address, @Context UriInfo uri) throws URISyntaxException { + LOGGER.info("RESTful call 'POST valid address'"); + if (addressesRepository.isValid(address)) { + LOGGER.fine("address is valid"); + return Response.ok().build(); + } else { + URI type = uri.getRequestUri().resolve("/errors/invalid-city"); + URI instance = UriBuilder.fromResource(getClass()).path(address.getCity().getZipCode().toString()).build(); + List suggestions = addressesRepository.findSuggestions(address.getCity()); + LOGGER.fine(suggestions.size() + " suggestions found: " + suggestions); + if (suggestions.size() == 1) { + return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) + .entity(String.format(PROBLEM_JSON, type, "invalid city", + Response.Status.BAD_REQUEST.getStatusCode(), + "Did you mean " + suggestions.iterator().next() + "?", instance)) + .build(); + } else if (!suggestions.isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) + .entity(String.format(PROBLEM_JSON, type, "invalid city", + Response.Status.BAD_REQUEST.getStatusCode(), + "Did you mean one of " + + suggestions.stream().map(Object::toString).collect(joining(", ")) + "?", + instance)) + .build(); + } else { + return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) + .entity(String.format(PROBLEM_JSON, type, "invalid city", + Response.Status.BAD_REQUEST.getStatusCode(), "no matching city found", instance)) + .build(); + } + } + } +} \ No newline at end of file diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 0000000..6b248ca --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +public class Address { + private Street street; + private City city; + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java new file mode 100644 index 0000000..df5bc5f --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +/** + * Address repository + */ +@ApplicationScoped +public class AddressRepository { + + private Logger LOGGER = Logger.getLogger(AddressRepository.class.getSimpleName()); + + private Set cities; + + @PostConstruct + public void initialize() { + cities = new HashSet<>(); + try (BufferedReader cityStream = new BufferedReader( + new InputStreamReader(getClass().getResourceAsStream("/plz.txt")))) { + for (String line = cityStream.readLine(); line != null; line = cityStream.readLine()) { + City city = new City(line); + for (CityName name : city.getCityNames()) { + this.cities.add(new City(city.getZipCode(), name)); + } + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + LOGGER.info(format("address validation repository initialized with %d cities: ", cities.size())); + } + + public boolean isValid(Address address) { + return cities.contains(address.getCity()); + } + + public List findSuggestions(City city) { + ZipCode zipCode = city.getZipCode(); + return cities.stream().filter(c -> c.getZipCode().equals(zipCode)).collect(toList()); + } +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 0000000..14de924 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + + +import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.Validate.notNull; + +import java.util.List; +import java.util.stream.Stream; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public static City valueOf(String name) { + return new City(name); + } + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + public City(ZipCode zipCode, CityName name) { + this(zipCode + " " + name); + } + + protected City() { + // for framework + } + + public ZipCode getZipCode() { + return new ZipCode(name.substring(0, 5)); + } + + public List getCityNames() { + String names = name.substring(5); + if (names.endsWith("u.a.")) { + names = names.substring(0, names.length() - "u.a.".length()); + } + return Stream.of(names.split(",")).map(CityName::new).collect(toList()); + } + + public CityName getCityName() { + return getCityNames().iterator().next(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City) object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 0000000..f307d92 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName) object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 0000000..68cbb1e --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + private String number; + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber number = (HouseNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 0000000..5563160 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street) object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 0000000..7d09755 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName)) { + return false; + } + StreetName name = (StreetName) object; + + return toString().equals(name.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 0000000..9709576 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode code = (ZipCode) object; + + return toString().equals(code.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java new file mode 100644 index 0000000..fbfce55 --- /dev/null +++ b/apps/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CORSFilter implements ContainerResponseFilter { + + @Override + public void filter(final ContainerRequestContext requestContext, + final ContainerResponseContext cres) throws IOException { + cres.getHeaders().add("Access-Control-Allow-Origin", "*"); + cres.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + cres.getHeaders().add("Access-Control-Allow-Credentials", "true"); + cres.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + cres.getHeaders().add("Access-Control-Max-Age", "1209600"); + } + +} diff --git a/apps/address-validation-service/src/main/resources/plz.txt b/apps/address-validation-service/src/main/resources/plz.txt new file mode 100644 index 0000000..3365250 --- /dev/null +++ b/apps/address-validation-service/src/main/resources/plz.txt @@ -0,0 +1,8219 @@ +04931 Mühlberg, Bad Liebenwerda +04932 Röderland, Großthiemig u.a. +04916 Herzberg/ Elster +03253 Doberlug-Kirchhain +04895 Falkenberg/ Elster +04938 Uebigau-Wahrenbrück +04924 Bad Liebenwerda +19309 Lenzen +19348 Perleberg, Berge u.a. +19322 Wittenberge, Rühstädt +19357 Karstädt, Dambeck, Klüß +14827 Wiesenburg +14806 Belzig +14797 Lehnin +14823 Niemegk +14778 Beetzsee, Wollin, Wenzlow, Golzow u.a. +14715 Milower Land, Schollene, Nennhausen u.a. +14776 Brandenburg/Havel +14828 Görzke +14793 Ziesar +14774 Brandenburg/ Havel +14789 Wusterwitz, Rosenau, Bensdorf +14798 Havelsee +14772 Brandenburg/ Havel +14770 Brandenburg/ Havel +15936 Dahme u.a. +15837 Baruth +15938 Golßen +12529 Schönefeld +14913 Jüterbog +14947 Nuthe-Urstromtal +14547 Beelitz +14929 Treuenbrietzen +14550 Groß Kreutz +04936 Schlieben +14822 Brück, Borkheide u.a. +14552 Michendorf +14542 Werder/ Havel +14554 Seddinger See +14548 Schwielowswee +14476 Potsdam +14469 Potsdam +14471 Potsdam +14959 Trebbin +14943 Luckenwalde +15806 Zossen +15838 Am Mellensee +14974 Ludwigsfelde +14532 Kleinmachnow +14482 Potsdam +14467 Potsdam +14558 Nuthetal +14473 Potsdam +14478 Potsdam +14480 Potsdam +15831 Blankenfelde-Mahlow +15834 Rangsdorf +15827 Blankenfelde-Mahlow +14979 Großbeeren +14513 Teltow +12209 Berlin-Lichterfelde +16928 Pritzwalk, Groß Pankow +16949 Triglitz, Putlitz +16945 Meyenburg, Kümmernitztal u.a. +16845 Neustadt (Dosse) u.a. +16909 Wittstock/Dosse, Heiligengrabe +16818 Fehrbellin, Temnitzquell, Märkisch Linden u.a. +16866 Gumtow, Kyritz u.a. +14662 Friesack +19339 Plattenburg +14727 Premnitz +14712 Rathenow +14728 Rhinow +19336 Legde/Quitzöbel, Bad Wilsnack +16868 Wusterhausen +14669 Ketzin +16831 Rheinsberg +16835 Lindow u.a. +16775 Gransee, Löwenberg +16348 Wandlitz +16727 Velten, Oberkrämer +17268 Templin, Boitzenburg u.a. +14621 Schönwalde +14641 Nauen +16766 Kremmen +16244 Schorfheide +16792 Zehdenick +16798 Fürstenberg +16515 Oranienburg, Mühlenbecker Land +14656 Brieselang +14612 Falkensee +14624 Dallgow-Döberitz +16833 Fehrbellin +16816 Neuruppin +16761 Hennigsdorf +16540 Hohen Neuendorf +13465 Berlin Frohnau +16567 Mühlenbecker Land +16548 Glienicke/Nordbahn +16552 Mühlenbecker Land +16556 Borgsdorf +16767 Leegebruch +16559 Liebenwalde +16547 Birkenwerder +16562 Hohen Neuendorf OT Bergfelde +16827 Neuruppin +16837 Rheinsberg +17279 Lychen +01945 Ruhland +01983 Großräschen +01979 Lauchhammer +03130 Spremberg, Tschernitz u.a. +03238 Finsterwalde +01990 Ortrand +04934 Hohenleipisch +04928 Plessa, Schraden +04910 Elsterwerda +01987 Schwarzheide N.L. +01998 Schipkau +01968 Senftenberg +01994 Schipkau +01996 Senftenberg +01993 Schipkau +03116 Drebkau +03103 Neu-Seeland, Neupetershain +03119 Welzow +03159 Döbern +03222 Lübbenau/ Spreewald +03249 Sonnewalde +03205 Calau, Bronkow +03185 Peitz +15306 Seelow, Lietzen u.a. +15746 Groß Köris +15757 Halbe +15913 Straupitz +15926 Luckau, Waldrehna, Heideblick, Fürstlich Drehna +15848 Beeskow +15518 Briesen, Rauen u.a. +15528 Spreenhagen +15859 Storkow +15537 Erkner +03246 Crinitz +15907 Lübben (Spreewald) +15910 Schönwalde +03229 Altdöbern, Luckaitztal +03226 Vetschau +03099 Kolkwitz +03096 Burg/Spreewald u.a. +15868 Lieberose +15749 Mittenwalde +15741 Bestensee +15755 Teupitz +15748 Märkisch Buchholz +15754 Heidesee +15745 Wildau +15738 Zeuthen +15711 Königs Wusterhausen +15732 Schulzendorf b. Eichenwade +15562 Rüdersdorf +15569 Woltersdorf +15713 Königs Wusterhausen +15712 Königs Wusterhausen +15526 Bad Saarow-Pieskow +15864 Wendisch Rietz +15517 Fürstenwalde/ Spree +03044 Cottbus +03197 Jänschwalde +03055 Cottbus +03042 Cottbus +03054 Cottbus +03046 Cottbus +03051 Cottbus +03052 Cottbus +03172 Guben, Schenkendöbern +15295 Brieskow-Finkenheerd +15890 Eisenhüttenstadt +15236 Treplin, Jacobsdorf, Frankfurt (Oder) +03058 Neuhausen/Spree +03048 Cottbus +03050 Cottbus +03053 Cottbus +03149 Forst/ Lausitz +15898 Neuzelle +15299 Müllrose +15234 Frankfurt/ Oder +15326 Zeschdorf, Podelzig, Lebus +15230 Frankfurt/ Oder +15232 Frankfurt/ Oder +15366 Neuenhagen, Hoppegarten +12623 Berlin Mahlsdorf +16341 Panketal +16230 Melchow, Chorin u.a. +16248 Oderberg u.a. +16356 Werneuchen +16321 Bernau +16247 Joachimsthal u.a. +16269 Wriezen +16259 Bad Freienwalde u.a. +15345 Lichtenow, Altlandsberg u.a. +16278 Angermünde +16306 Biesendahlshof, Berkholz-Meyenburg +17291 Prenzlau, Nordwestuckermark u.a. +15374 Müncheberg +16303 Schwedt +15370 Fredersdorf-Vogelsdorf, Petershagen +15378 Rüdersdorf +15566 Schöneiche bei Berlin +15344 Strausberg +16359 Biesenthal +16225 Eberswalde +16227 Eberswalde +15377 Oberbarnim, Märkische Höhe u.a. +15320 Neutrebbin, Neuhardenberg +15328 Golzow, Zechin u.a. +15324 Letschin +16307 Gartz (Oder) +17337 Uckerland, Groß Luckow, Schönhausen +17326 Brüssow +14109 Berlin Wannsee +14129 Berlin Nikolassee +14089 Berlin Gatow +12307 Berlin +12277 Berlin +12107 Berlin Mariendorf +14195 Berlin Dahlem +12105 Berlin Mariendorf +12109 Berlin Mariendorf +14167 Berlin Zehlendorf +12347 Berlin Britz +14169 Berlin Zehlendorf +14165 Berlin Zehlendorf +14163 Berlin Zehlendorf +12209 Berlin-Lichterfelde +12207 Berlin Lichtenfelde +12205 Berlin Lichtenfelde +12203 Berlin Lichtenfelde +12309 Berlin +12305 Berlin +12249 Berlin Lankwitz +12279 Berlin +12247 Berlin Lankwitz +12167 Berlin Steglitz +12349 Berlin Buckow +14055 Berlin Westend +14193 Berlin Grunewald +13587 Berlin Hakenfelde +13585 Berlin Spandau +13503 Berlin Tegel +13589 Berlin Falkenhagener Feld +13505 Berlin Tegel +13599 Berlin Haselhorst +13581 Berlin Spandau +13593 Berlin Wilhelmstadt +13591 Berlin Staaken +13583 Berlin Spandau +13597 Berlin Spandau +13595 Berlin Wlhelmstadt +14050 Berlin Westend +10249 Berlin Friedrichshain +10965 Berlin Kreuzberg +10115 Berlin Mitte +10785 Berlin Tiergarten +13158 Berlin Rosenthal +13127 Berlin Französisch Buchholz +13159 Berlin Blankenfelde +13359 Berlin Gesundbrunnen +13357 Berlin Gesundbrunnen +13469 Berlin Lübars +13465 Berlin Frohnau +13467 Berlin Hermsdorf +13405 Berlin Wedding +13437 Berlin Reinickendorf +13507 Berlin Tegel +12165 Berlin Steglitz +12163 Berlin Steglitz +14197 Berlin Wilmersdorf +14199 Berlin Schmargendorf +10709 Berlin Wilmersdorf +10711 Berlin Halensee +10713 Berlin Wilmersdorf +10707 Berlin Wilmersdorf +14052 Berlin Westend +14053 Berlin Westend +13629 Berlin Siemensstadt +14057 Berlin Charlottenburg +14059 Berlin Charlottenburg +10553 Berlin Moabit +10629 Berlin Charlottenburg +10627 Berlin Charlottenburg +10587 Berlin Charlottenburg +10625 Berlin Charlottenburg +10589 Berlin Charlottenburg +10585 Berlin Charlottenburg +13351 Berlin Wedding +13627 Berlin Charlottenburg-Nord +12169 Berlin Steglitz +12157 Berlin Schöneberg +12161 Berlin Friedenau +12159 Berlin Friedenau +10715 Berlin Wilhelmsdorf +10827 Berlin +10825 Berlin +10829 Berlin Schöneberg +12103 Berlin Tempelhof +12101 Berlin Tempelhof +10823 Berlin-West +10719 Berlin Wilmersdorf +10717 Berlin Wilmersdorf +10779 Berlin Schöneberg +10777 Berlin Wilmersdorf +10783 Berlin Schöneberg +10781 Berlin Schöneberg +12099 Berlin Tempelhof +12051 Berlin Neukölln +12047 Berlin Kreuzberg +12049 Berlin Neukölln +10999 Berlin Kreuzberg +10967 Berlin Kreuzberg +10961 Berlin Kreuzberg +10969 Berlin Kreuzberg +10997 Berlin Kreuzberg +10963 Berlin Kreuzberg +10551 Berlin Moabit +10555 Berlin Moabit +10557 Berlin Moabit +10559 Berlin Moabit +10623 Berlin Charlottenburg +10787 Berlin Tiergarten +13353 Berlin Wedding +13347 Berlin Wedding +10789 Berlin Schöneberg +10405 Berlin Prenzlauer Berg +10437 Berlin Prenzlauer Berg +10439 Berlin Prenzlauer Berg +10435 Berlin Prenzlauer Berg +10178 Berlin Mitte +10179 Berlin Mitte +10119 Berlin Mitte +10117 Berlin Mitte +13355 Berlin Wedding +13403 Berlin Reinickendorf +13509 Berlin Reinickendorf +13349 Berlin Wedding +13435 Berlin Märkisches Viertel +13407 Berlin-West +13409 Berlin-West +13439 Berlin Märkisches Viertel +13189 Berlin Pankow +13156 Berlin Niederschönhausen +13187 Berlin Pankow +15537 Erkner +12557 Berlin +12559 Berlin Köpenick +12524 Berlin Altglienicke +12527 Berlin Schmöckwitz +12587 Berlin Wiesengrund +12526 Berlin Bohnsdorf +12555 Berlin Köpenik +12489 Berlin Teltowkanal III +12439 Berlin Niederschöneweide +12355 Berlin Rudow +12357 Berlin Rudow +12437 Berlin Baumschulenweg +12353 Berlin Gropiusstadt +12351 Berlin Buckow +12359 Berlin Britz +12487 Berlin +12589 Berlin Rahnsdorf +12435 Berlin Alt Treptow +12057 Berlin Neukölln +12045 Berlin Neukölln +12053 Berlin Neukölln +12059 Berlin Neukölln +12055 Berlin Neukölln +12043 Berlin Neukölln +10245 Berlin Friedrichshain +12623 Berlin Mahlsdorf +12621 Berlin Kaulsdorf +12619 Berlin +12689 Berlin +12681 Berlin +12679 Berlin +12459 Berlin Oberschöneweide +10318 Berlin Karlshorst +10319 Berlin +10317 Berlin Rummelsburg +10365 Berlin Lichtenberg +13055 Berlin Alt-Hohenschönhausen +13053 Berlin Alt-Hohenschönhausen +13088 Berlin Weißensee +13057 Berlin Falkenberg +13125 Berlin Buch +10367 Berlin Lichtenberg +10369 Berlin Lichtenberg +10247 Berlin Friedrichshain +10243 Berlin Friedrichshain +10407 Berlin Prenzlauer Berg +10409 Berlin Prenzlauer Berg +13086 Berlin Weißensee +10315 Berlin Friedrichsfelde +12683 Berlin Biesdorf +12685 Berlin +12629 Berlin +12627 Berlin Hellersdorf +13089 Berlin Heinelsdorf +13129 Berlin Blankenburg +13059 Berlin Wartenberg +13051 Berlin Neu-Schönhausen +12687 Berlin +79588 Efringen-Kirchen +79415 Bad Bellingen +79395 Neuenburg am Rhein +79539 Lörrach +79541 Lörrach +79576 Weil am Rhein +79589 Binzen +79585 Steinen +79594 Inzlingen +79618 Rheinfelden (Baden) +79639 Grenzach-Wyhlen +78713 Schramberg +78052 Villingen-Schwenningen +77955 Ettenheim +79761 Waldshut-Tiengen +79341 Kenzingen +79336 Herbolzheim +78166 Donaueschingen +78199 Bräunlingen +77761 Schiltach +79591 Eimeldingen +79540 Lörrach +79650 Schopfheim +79664 Wehr +79736 Rickenbach +79713 Bad Säckingen +79739 Schwörstadt +79592 Fischingen +79400 Kandern +79429 Malsburg-Marzell +79418 Schliengen +79692 Kleines Wiesental +79424 Auggen +79379 Müllheim +79689 Maulburg +79244 Münstertal +79219 Staufen im Breisgau +79295 Sulzburg +79410 Badenweiler +79597 Schallbach +79595 Rümmingen +79599 Wittlingen +79677 Schönau im Schwarzwald +79669 Zell im Wiesental +79674 Todtnau +79685 Häg-Ehrsberg +79688 Hausen im Wiesental +79737 Herrischried +79682 Todtmoos +79694 Utzenfeld +79695 Wieden +79686 Hasel +79725 Laufenburg (Baden) +79730 Murg +79733 Görwihl +79774 Albbruck +79804 Dogern +79790 Küssaberg +79801 Hohentengen am Hochrhein +79771 Klettgau +79787 Lauchringen +79837 St. Blasien, Ibach +79875 Dachsberg +79862 Höchenschwand +79809 Weilheim +79872 Bernau im Schwarzwald +79859 Schluchsee +79793 Wutöschingen +79805 Eggingen +79777 Ühlingen-Birkendorf +79865 Grafenhausen +79848 Bonndorf im Schwarzwald +79780 Stühlingen +79879 Wutach +79112 Freiburg im Breisgau +79111 Freiburg im Breisgau +79206 Breisach am Rhein +79426 Buggingen +79423 Heitersheim +79427 Eschbach +79258 Hartheim +79189 Bad Krozingen +79227 Schallstadt +79238 Ehrenkirchen +79232 March +79224 Umkirch +79288 Gottenheim +79241 Ihringen +79282 Ballrechten-Dottingen +79292 Pfaffenweiler +79291 Merdingen +79100 Freiburg im Breisgau +79117 Freiburg im Breisgau +79104 Freiburg im Breisgau +79108 Freiburg im Breisgau +79194 Gundelfingen, Heuweiler +79254 Oberried +79283 Bollschweil +79299 Wittnau +79289 Horben +79280 Au (Breisgau) +79199 Kirchzarten +79252 Stegen +79286 Glottertal +79285 Ebringen +79294 Sölden +79115 Freiburg im Breisgau +79114 Freiburg im Breisgau +79249 Merzhausen +79102 Freiburg im Breisgau +79110 Freiburg im Breisgau +79098 Freiburg im Breisgau +79106 Freiburg im Breisgau +77977 Rust +77975 Ringsheim +79356 Eichstetten +79235 Vogtsburg im Kaiserstuhl +79361 Sasbach am Kaiserstuhl +79346 Endingen am Kaiserstuhl +79353 Bahlingen am Kaiserstuhl +79331 Teningen +79365 Rheinhausen +79362 Forchheim +79369 Wyhl +79359 Riegel Kaiserstuhl +79367 Weisweil +79268 Bötzingen +79183 Waldkirch +79261 Gutach im Breisgau +79350 Sexau +79348 Freiamt +79211 Denzlingen +79312 Emmendingen +79279 Vörstetten +79276 Reute +79364 Malterdingen +77978 Schuttertal +79868 Feldberg +79853 Lenzkirch +79822 Titisee-Neustadt +79874 Breitnau +79856 Hinterzarten +79256 Buchenbach +79274 St. Märgen +79271 Sankt Peter +79263 Simonswald +78148 Gütenbach +78120 Furtwangen im Schwarzwald +79843 Löffingen +79877 Friedenweiler +79871 Eisenbach +78147 Vöhrenbach +77716 Fischerbach, Haslach, Hofstetten +79215 Elzach, Biederbach +77709 Wolfach, Oberwolfach +79297 Winden im Elztal +78141 Schönwald im Schwarzwald +78136 Schonach im Schwarzwald +77793 Gutach (Schwarzwaldbahn) +77796 Mühlenbach +77756 Hausach +77790 Steinach +78144 Schramberg +78098 Triberg im Schwarzwald +78089 Unterkirnach +78112 St. Georgen +78126 Königsfeld im Schwarzwald +78739 Hardt +78132 Hornberg +78730 Lauterbach +78733 Aichhalden +79802 Dettighofen +79807 Lottstetten +88605 Meßkirch, Sauldorf +88630 Pfullendorf +78176 Blumberg +78234 Engen +78187 Geisingen +72419 Neufra +78600 Kolbingen +72488 Sigmaringen +79798 Jestetten +78250 Tengen +78259 Mühlhausen-Ehingen +78247 Hilzingen +78244 Gottmadingen +78239 Rielasingen-Worblingen +78262 Gailingen am Hochrhein +78267 Aach +78224 Singen +78337 Öhningen +78345 Moos +78343 Gaienhofen +78315 Radolfzell am Bodensee +78256 Steißlingen +78359 Orsingen-Nenzingen +78333 Stockach +78351 Bodman-Ludwigshafen +78269 Volkertshausen +88682 Salem +88699 Frickingen +88690 Uhldingen-Mühlhofen +88696 Owingen +78467 Konstanz +78465 Konstanz +78464 Konstanz +88662 Überlingen +78476 Allensbach +78479 Reichenau +78462 Konstanz +78354 Sipplingen +78056 Villingen-Schwenningen +78050 Villingen-Schwenningen +78086 Brigachtal +78073 Bad Dürrheim +78609 Tuningen +78183 Hüfingen +78591 Durchhausen +78607 Talheim +78194 Immendingen +78532 Tuttlingen +78573 Wurmlingen +78604 Rietheim-Weilheim +78589 Dürbheim +78606 Seitingen-Oberflacht +78549 Spaichingen +78594 Gunningen +78647 Trossingen +78595 Hausen ob Verena +78048 Villingen-Schwenningen +78628 Rottweil +78667 Villingendorf +78652 Deißlingen +78078 Niedereschach +78655 Dunningen +78658 Zimmern ob Rottweil +78664 Eschbronn +78736 Epfendorf +78662 Bösingen +78087 Mönchweiler +78054 Villingen-Schwenningen +78083 Dauchingen +78564 Wehingen, Reichenbach +78582 Balgheim +78554 Aldingen +78588 Denkingen +78559 Gosheim +78585 Bubsheim +72336 Balingen +72348 Rosenfeld +78583 Böttingen +78601 Mahlstetten +72365 Ratshausen +72355 Schömberg +72356 Dautmergen +72369 Zimmern unter der Burg +78661 Dietingen +78586 Deilingen +78669 Wellendingen +78665 Frittlingen +72367 Weilen unter den Rinnen +72358 Dormettingen +72359 Dotternhausen +88637 Leibertingen, Buchheim +88631 Beuron +78253 Eigeltingen +78357 Mühlingen +78576 Emmingen-Liptingen +78579 Neuhausen ob Eck +78567 Fridingen an der Donau +78570 Mühlheim an der Donau +88634 Herdwangen-Schönach +88639 Wald +78355 Hohenfels +72505 Krauchenwies +72514 Inzigkofen +72461 Albstadt +72458 Albstadt +72459 Albstadt +78592 Egesheim +78598 Königsheim +78603 Renquishausen +78580 Bärenthal +78597 Irndorf +72362 Nusplingen +72469 Meßstetten +72477 Schwenningen +72510 Stetten am kalten Markt +72364 Obernheim +72361 Hausen am Tann +72475 Bitz +72501 Gammertingen +72513 Hettingen +72474 Winterlingen +72479 Straßberg +72519 Veringenstadt +72511 Bingen +88368 Bergatreute +88239 Wangen im Allgäu +88633 Heiligenberg +88276 Berg +88410 Bad Wurzach +88427 Bad Schussenried +88529 Zwiefalten +89584 Ehingen (Donau), Lauterach +88512 Mengen +88518 Herbertingen +72534 Hayingen +88069 Tettnang +88079 Kressbronn am Bodensee +88085 Langenargen +88097 Eriskirch +88271 Wilhelmsdorf +88709 Meersburg +88045 Friedrichshafen +88048 Friedrichshafen +88636 Illmensee +88693 Deggenhausertal +88263 Horgenzell +88677 Markdorf +88094 Oberteuringen +88697 Bermatingen +88090 Immenstaad am Bodensee +88718 Daisendorf +88719 Stetten +88250 Weingarten +88212 Ravensburg +88074 Meckenbeuren +88046 Friedrichshafen +88255 Baienfurt +88214 Ravensburg +88287 Grünkraut +88213 Ravensburg +88281 Schlier +88147 Achberg +88167 Röthenbach (Allgäu) +88289 Waldburg +88279 Amtzell +88353 Kißlegg +88267 Vogt +88364 Wolfegg +88099 Neukirch +88285 Bodnegg +88299 Leutkirch im Allgäu +88260 Argenbühl +88316 Isny im Allgäu +88374 Hoßkirch +88348 Bad Saulgau, Allmannsweiler +88356 Ostrach +88367 Hohentengen +88377 Riedhausen +88376 Königseggwald +88373 Fleischwangen +88361 Altshausen +88371 Ebersbach-Musbach +88370 Ebenweiler +88379 Unterwaldhausen +88284 Wolpertswende +88273 Fronreute +88422 Bad Buchau +88326 Aulendorf +72517 Sigmaringendorf +88515 Langenenslingen +88521 Ertingen +88499 Riedlingen +72539 Pfronstetten +72516 Scheer +88527 Unlingen +88525 Dürmentingen +88524 Uttenweiler +89597 Munderkingen +89611 Obermarchtal +89613 Oberstadion +89607 Emerkingen +89617 Untermarchtal +88339 Bad Waldsee +88456 Ingoldingen +88444 Ummendorf +88436 Eberhardzell +88454 Hochdorf +88459 Tannheim +88319 Aitrach +88450 Berkheim +88430 Rot an der Rot +88416 Ochsenhausen +88457 Kirchdorf an der Iller +88317 Aichstetten +88448 Attenweiler +88447 Warthausen +88487 Mietingen +88433 Schemmerhofen +88400 Biberach an der Riß +88437 Maselheim +88471 Laupheim +89619 Unterstadion +88441 Mittelbiberach +89616 Rottenacker +89608 Griesingen +88489 Wain +88451 Dettingen an der Iller +88480 Achstetten +88477 Schwendi +88453 Erolzheim +88483 Burgrieden +88486 Kirchberg an der Iller +88481 Balzheim +88484 Gutenzell-Hürbel +89165 Dietenheim +89186 Illerrieden +89194 Schnürpflingen +77966 Kappel-Grafenhausen +77743 Neuried +77974 Meißenheim +77963 Schwanau +77740 Bad Peterstal-Griesbach +76596 Forbach +72250 Freudenstadt +77656 Offenburg +77654 Offenburg +77652 Offenburg +77972 Mahlberg +77933 Lahr/Schwarzwald +77971 Kippenheim +77960 Seelbach +77791 Berghaupten +77749 Hohberg +77948 Friesenheim +77746 Schutterwald +77770 Durbach +77797 Ohlsbach +77799 Ortenberg +77855 Achern (Abweichung Exklaven) +77866 Rheinau +77871 Renchen +77767 Appenweier +77731 Willstätt +77694 Kehl +77704 Oberkirch +77781 Biberach +77736 Zell am Harmersbach +77723 Gengenbach +77784 Oberharmersbach +77787 Nordrach +77728 Oppenau +77776 Bad Rippoldsau-Schapbach +77773 Schenkenzell +72275 Alpirsbach +72290 Loßburg +76534 Baden-Baden +77833 Ottersweier +77815 Bühl +77830 Bühlertal +77880 Sasbach +77887 Sasbachwalden +77886 Lauf +77876 Kappelrodeck +77794 Lautenbach +77883 Ottenhöfen im Schwarzwald +77889 Seebach +76593 Gernsbach +72297 Seewald +72270 Baiersbronn +77836 Rheinmünster +77839 Lichtenau +76532 Baden-Baden +76547 Sinzheim +76437 Rastatt +76473 Iffezheim +76549 Hügelsheim +76332 Bad Herrenalb +76597 Loffenau +76530 Baden-Baden +76316 Malsch +76275 Ettlingen +76571 Gaggenau +76461 Muggensturm +76470 Ötigheim +76456 Kuppenheim +76599 Weisenbach +76476 Bischweier +76479 Steinmauern +76477 Elchesheim-Illingen +76187 Karlsruhe +76149 Karlsruhe +76287 Rheinstetten +76185 Karlsruhe +76137 Karlsruhe +76199 Karlsruhe +76344 Eggenstein-Leopoldshafen +76189 Karlsruhe +76467 Bietigheim +76448 Durmersheim +76474 Au am Rhein +76135 Karlsruhe +76133 Karlsruhe +76661 Philippsburg +76351 Linkenheim-Hochstetten +76706 Dettenheim +78727 Oberndorf am Neckar +78737 Fluorn-Winzeln +72172 Sulz am Neckar +72280 Dornstetten +72296 Schopfloch +72293 Glatten +72175 Dornhan +71032 Böblingen +74336 Brackenheim +72072 Tübingen +72818 Trochtelfingen +72116 Mössingen +72108 Rottenburg am Neckar +72160 Horb am Neckar +72184 Eutingen im Gäu +72178 Waldachtal +72401 Haigerloch +72351 Geislingen +72181 Starzach +72186 Empfingen +72189 Vöhringen +75337 Enzklösterle +72226 Simmersfeld +72213 Altensteig +72294 Grömbach +72227 Egenhausen +72221 Haiterbach +72285 Pfalzgrafenweiler +75323 Bad Wildbad +75389 Neuweiler +72299 Wörnersberg +75387 Neubulach +75391 Gechingen +72202 Nagold +71083 Herrenberg +71126 Gäufelden +71131 Jettingen +71149 Bondorf +71159 Mötzingen +72218 Wildberg +72224 Ebhausen +72229 Rohrdorf +75392 Deckenpfronn +75385 Bad Teinach-Zavelstein +75365 Calw +72131 Ofterdingen +72144 Dußlingen +72145 Hirrlingen +72411 Bodelshausen +72379 Hechingen +72417 Jungingen +72406 Bisingen +72415 Grosselfingen +72414 Rangendingen +72762 Reutlingen +72770 Reutlingen +72793 Pfullingen +72805 Lichtenstein +72820 Sonnenbühl +72147 Nehren +72810 Gomaringen +72393 Burladingen +72149 Neustetten +71034 Böblingen +72070 Tübingen +72076 Tübingen +71088 Holzgerlingen +71155 Altdorf +72119 Ammerbuch +71116 Gärtringen +71134 Aidlingen +71154 Nufringen +71157 Hildrizhausen +71139 Ehningen +70771 Leinfelden-Echterdingen +70794 Filderstadt +72074 Tübingen +72766 Reutlingen +72768 Reutlingen +72127 Kusterdingen +72138 Kirchentellinsfurt +72124 Pliezhausen +72827 Wannweil +72654 Neckartenzlingen +72141 Walddorfhäslach +72657 Altenriet +72631 Aichtal +71111 Waldenbuch +71093 Weil im Schönbuch +71101 Schönaich +72764 Reutlingen +72760 Reutlingen +72135 Dettenhausen +71144 Steinenbronn +72667 Schlaitdorf +76359 Marxzell +76307 Karlsbad +75334 Straubenhardt +75305 Neuenbürg +75210 Keltern +75217 Birkenfeld +75335 Dobel +75328 Schömberg +75394 Oberreichenbach +75339 Höfen an der Enz +75449 Wurmberg +75181 Pforzheim +75173 Pforzheim +75179 Pforzheim +75180 Pforzheim +75233 Tiefenbronn +75175 Pforzheim +75331 Engelsbrand +71299 Wimsheim +75378 Bad Liebenzell +75242 Neuhausen +71292 Friolzheim +75382 Althengstett +75397 Simmozheim +71263 Weil der Stadt +75399 Unterreichenbach +76131 Karlsruhe +76228 Karlsruhe +76139 Karlsruhe +76327 Pfinztal +76227 Karlsruhe +76646 Bruchsal +76356 Weingarten +76337 Waldbronn +76297 Stutensee +76229 Karlsruhe +75196 Remchingen +75045 Walzbachtal +75203 Königsbach-Stein +75236 Kämpfelbach +76703 Kraichtal +75438 Knittlingen +75433 Maulbronn +75223 Niefern-Öschelbronn +75015 Bretten +75228 Ispringen +75249 Kieselbronn +75038 Oberderdingen +75417 Mühlacker +75248 Ölbronn-Dürrn +75245 Neulingen +75053 Gondelsheim +75239 Eisingen +75059 Zaisenhausen +75177 Pforzheim +75443 Ötisheim +75172 Pforzheim +71065 Sindelfingen +71067 Sindelfingen +71069 Sindelfingen +75446 Wiernsheim +71106 Magstadt +71229 Leonberg +71272 Renningen +71277 Rutesheim +71287 Weissach +71254 Ditzingen +71282 Hemmingen +71735 Eberdingen +71297 Mönsheim +75395 Ostelsheim +71120 Grafenau +71063 Sindelfingen +71296 Heimsheim +70599 Stuttgart +70327 Stuttgart +70329 Stuttgart +70184 Stuttgart +70839 Gerlingen +70569 Stuttgart +70197 Stuttgart +70825 Korntal-Münchingen +70567 Stuttgart +70565 Stuttgart +70195 Stuttgart +70806 Kornwestheim +70437 Stuttgart +71638 Ludwigsburg +71636 Ludwigsburg +71686 Remseck am Neckar +73760 Ostfildern +71696 Möglingen +71701 Schwieberdingen +70563 Stuttgart +70629 Stuttgart/Leinfelden-Echterdingen +70173 Stuttgart +70597 Stuttgart +70199 Stuttgart +70180 Stuttgart +70178 Stuttgart +70193 Stuttgart +70176 Stuttgart +70619 Stuttgart +70186 Stuttgart +70182 Stuttgart +70499 Stuttgart +70435 Stuttgart +70192 Stuttgart +70174 Stuttgart +70191 Stuttgart +70469 Stuttgart +70378 Stuttgart +70376 Stuttgart +70190 Stuttgart +70374 Stuttgart +70188 Stuttgart +70372 Stuttgart +70439 Stuttgart +74363 Güglingen +74389 Cleebronn +74343 Sachsenheim +74357 Bönnigheim +74397 Pfaffenhofen +74374 Zaberfeld +75031 Eppingen +75057 Kürnbach +75056 Sulzfeld +75428 Illingen +75447 Sternenfels +71706 Markgröningen +71665 Vaihingen an der Enz +71739 Oberriexingen +74372 Sersheim +74395 Mundelsheim +74385 Pleidelsheim +74321 Bietigheim-Bissingen +74379 Ingersheim +74382 Neckarwestheim +74348 Lauffen am Neckar +74223 Flein +74360 Ilsfeld +74388 Talheim +74369 Löchgau +74354 Besigheim +74399 Walheim +74366 Kirchheim am Neckar +74391 Erligheim +71642 Ludwigsburg +71672 Marbach am Neckar +71711 Steinheim, Murr +71691 Freiberg am Neckar +71732 Tamm +71634 Ludwigsburg +71679 Asperg +71640 Ludwigsburg +71726 Benningen am Neckar +74392 Freudental +74394 Hessigheim +74376 Gemmrigheim +72800 Eningen +72813 St. Johann +72532 Gomadingen +72829 Engstingen +72531 Hohenstein +72574 Bad Urach +89079 Ulm +89150 Laichingen +89160 Dornstadt +89558 Böhmenkirch +72525 Münsingen +73453 Abtsgmünd +73252 Lenningen +74426 Bühlerzell +73479 Ellwangen (Jagst) +73489 Jagstzell +74564 Crailsheim +74523 Schwäbisch Hall +74545 Michelfeld +74535 Mainhardt +89604 Allmendingen +89601 Schelklingen +72537 Mehrstetten +72535 Heroldstatt +72581 Dettingen an der Erms +72555 Metzingen +72658 Bempflingen +72582 Grabenstetten +72584 Hülben +72639 Neuffen +73277 Owen +72636 Frickenhausen +72660 Beuren +72622 Nürtingen +72666 Neckartailfingen +73230 Kirchheim unter Teck +73240 Wendlingen am Neckar +73249 Wernau (Neckar) +73257 Köngen +73765 Neuhausen auf den Fildern +73770 Denkendorf +72649 Wolfschlugen +73274 Notzingen +72585 Riederich +72664 Kohlberg +72661 Grafenberg +73268 Erkenbrechtsweiler +72655 Altdorf +72663 Großbettlingen +72669 Unterensingen +73265 Dettingen unter Teck +72644 Oberboihingen +73037 Göppingen +73035 Göppingen +73345 Hohenstadt/Drackenstein +72587 Römerstein +72589 Westerheim +73349 Wiesensteig +73235 Weilheim an der Teck +73272 Neidlingen +73344 Gruibingen +73347 Mühlhausen im Täle +73092 Heiningen +73087 Bad Boll +73101 Aichelberg +73266 Bissingen an der Teck +73278 Schlierbach +73110 Hattenhofen +73275 Ohmden +73271 Holzmaden +73119 Zell unter Aichelberg +73105 Dürnau +73108 Gammelshausen +89143 Blaubeuren +89155 Erbach +89605 Altheim +89180 Berghülen +89614 Öpfingen +89134 Blaustein +89610 Oberdischingen +89077 Ulm +89195 Staig +89171 Illerkirchberg +89129 Langenau +89081 Ulm +89075 Ulm +89185 Hüttisheim +89073 Ulm +73072 Donzdorf +89191 Nellingen +89173 Lonsee +89188 Merklingen +73340 Amstetten +73342 Bad Ditzenbach +73312 Geislingen an der Steige +73326 Deggingen +73079 Süßen +73329 Kuchen +73337 Bad Überkingen +73333 Gingen an der Fils +73114 Schlat +73107 Eschenbach +89177 Ballendorf +89179 Beimerstetten +89197 Weidenstetten +89189 Neenstetten +89183 Breitingen +89174 Altheim (Alb) +89555 Steinheim am Albuch +89547 Gerstetten +89198 Westerstetten +89182 Bernstadt +73734 Esslingen am Neckar +73730 Esslingen am Neckar +73733 Esslingen am Neckar +70736 Fellbach +70734 Fellbach +71336 Waiblingen +71334 Waiblingen +73663 Berglen +71404 Korb +73207 Plochingen +73779 Deizisau +71384 Weinstadt +71394 Kernen im Remstal +73666 Baltmannsweiler +73650 Winterbach +73630 Remshalden +71364 Winnenden +73728 Esslingen am Neckar +73732 Esslingen am Neckar +73776 Altbach +73773 Aichwald +71332 Waiblingen +71409 Schwaikheim +73269 Hochdorf +73262 Reichenbach an der Fils +73033 Göppingen +73553 Alfdorf, Schillinghof +73642 Welzheim +73655 Plüderhausen +73660 Urbach +73614 Schorndorf +73061 Ebersbach an der Fils +73066 Uhingen +73095 Albershausen +73098 Rechberghausen +73099 Adelberg +73117 Wangen +73547 Lorch +73635 Rudersberg +73669 Lichtenwald +73104 Börtlingen +74245 Löwenstein +74232 Abstatt +74199 Untergruppenbach +71717 Beilstein +71543 Wüstenrot, Beilstein-Stocksberg +71729 Erdmannhausen +71737 Kirchberg +71720 Oberstenfeld +71723 Großbottwar +71522 Backnang +71397 Leutenbach +71563 Affalterbach +71576 Burgstetten +71546 Aspach +71579 Spiegelberg +71570 Oppenweiler +71573 Allmersbach im Tal +71540 Murrhardt +73667 Kaisersbach +71554 Weissach im Tal +71566 Althütte +71560 Sulzbach an der Murr +71577 Großerlach +71549 Auenwald +74538 Rosengarten +74420 Oberrot +73529 Stadt Schwäbisch Gmünd +73525 Stadt Schwäbisch Gmünd +73527 Schwäbisch Gmünd, Täferrot +73569 Eschach, Obergröningen +73550 Waldstetten +73113 Ottenbach +73111 Lauterstein +73565 Spraitbach +73054 Eislingen/Fils +73084 Salach +73557 Mutlangen +73575 Leinzell +73574 Iggingen +73577 Ruppertshofen +73102 Birenbach +73116 Wäschenbeuren +73568 Durlangen, Weggen-Ziegelhütte, Leinhäusle +73430 Aalen +73431 Aalen +73434 Aalen +73433 Aalen +89551 Königsbronn +73460 Hüttlingen +73457 Essingen +73540 Heubach +73566 Bartholomä +73579 Schechingen +73560 Böbingen an der Rems +73563 Mögglingen +73447 Oberkochen +73571 Göggingen +73572 Heuchlingen +74427 Fichtenberg +74405 Gaildorf +74417 Gschwend +74429 Sulzbach-Laufen +74423 Obersontheim +74541 Vellberg +74544 Michelbach an der Bilz +73491 Neuler +74424 Bühlertann +73486 Adelmannsfelden +73494 Rosenberg +74597 Stimpfach +74586 Frankenhardt +76698 Ubstadt-Weiher +76669 Bad Schönborn +76694 Forst +76689 Karlsdorf-Neuthard +76709 Kronau +76707 Hambrücken +76676 Graben-Neudorf +68753 Waghäusel +68794 Oberhausen-Rheinhausen +68789 Sankt Leon-Rot +68804 Altlußheim +69190 Walldorf +68799 Reilingen +74722 Buchen +68809 Neulußheim +76684 Östringen +69234 Dielheim +69168 Wiesloch +74939 Zuzenhausen +74889 Sinsheim +74918 Angelbachtal +69254 Malsch +69242 Mühlhausen +69231 Rauenberg +68782 Brühl +68542 Heddesheim +68219 Mannheim +68259 Mannheim +68723 Schwetzingen +68309 Mannheim +68775 Ketsch +68199 Mannheim +68239 Mannheim +68766 Hockenheim +68163 Mannheim +68526 Ladenburg +68169 Mannheim +68549 Ilvesheim +69123 Heidelberg +67166 Otterstadt +68159 Mannheim +68161 Mannheim +68167 Mannheim +68165 Mannheim +68535 Edingen-Neckarhausen +68229 Mannheim +69251 Gaiberg +69117 Heidelberg +69469 Weinheim +69198 Schriesheim +69221 Dossenheim +69257 Wiesenbach +69253 Heiligkreuzsteinach +69118 Heidelberg +69115 Heidelberg +69259 Wilhelmsfeld +69245 Bammental +69181 Leimen +69207 Sandhausen +69121 Heidelberg +69434 Hirschhorn, Brombach, Heddesbach +69151 Neckargemünd +69493 Hirschberg an der Bergstraße +69250 Schönau +74909 Meckesheim +69226 Nußloch +69214 Eppelheim +69124 Heidelberg +69126 Heidelberg +69256 Mauer +69120 Heidelberg +74193 Schwaigern +74252 Massenbachhausen +74906 Bad Rappenau +74930 Ittlingen +74912 Kirchardt +75050 Gemmingen +74915 Waibstadt +74924 Neckarbischofsheim +74927 Eschelbronn +74921 Helmstadt-Bargen +74177 Bad Friedrichshall +74211 Leingarten +74074 Heilbronn +74206 Bad Wimpfen +74226 Nordheim +74080 Heilbronn +74936 Siegelsbach +74078 Heilbronn +74172 Neckarsulm +74229 Oedheim +74081 Heilbronn +74831 Gundelsheim +74254 Offenau +74076 Heilbronn +74855 Haßmersheim +74928 Hüffenhardt +74842 Billigheim +74072 Heilbronn +74257 Untereisesheim +69412 Eberbach +69436 Schönbrunn +69439 Zwingenberg +74925 Epfenbach +74931 Lobbach +74858 Aglasterhausen +74867 Neunkirchen +74869 Schwarzach +74847 Obrigheim +74933 Neidenstein +74937 Spechbach +74934 Reichartshausen +69427 Mudau +69429 Waldbrunn +69437 Neckargerach +74821 Mosbach +74865 Neckarzimmern +74834 Elztal +74838 Limbach +74864 Fahrenbach +74862 Binau +68305 Mannheim +68519 Viernheim +68307 Mannheim +69514 Laudenbach +69502 Hemsbach +64646 Heppenheim (Bergstraße) +64754 Badisch Schöllenbach +74243 Langenbrettach +74249 Jagsthausen +74239 Hardthausen am Kocher +74248 Ellhofen +74219 Möckmühl +74189 Weinsberg +74861 Neudenau +74251 Lehrensteinsfeld +74259 Widdern +74246 Eberstadt +74182 Obersulm +74196 Neuenstadt am Kocher +74626 Bretzfeld +97877 Wertheim +97922 Lauda-Königshofen +97996 Niederstetten +97990 Weikersheim +74613 Öhringen +74673 Mulfingen +74572 Blaufelden +74575 Schrozberg +74214 Schöntal +74731 Walldürn +74736 Hardheim +74235 Erlenbach +74653 Künzelsau, Ingelfingen +74629 Pfedelbach +74635 Kupferzell +74632 Neuenstein +74638 Waldenburg +74679 Weißbach +74670 Forchtenberg +74676 Niedernhall +74639 Zweiflingen +74255 Roigheim +74850 Schefflenz +74706 Osterburken +74743 Seckach +74740 Adelsheim +74749 Rosenberg +97944 Boxberg +97959 Assamstadt +74238 Krautheim +74747 Ravenstein +74744 Ahorn +74532 Ilshofen +74542 Braunsbach +74549 Wolpertshausen +74547 Untermünkheim +74595 Langenburg +74585 Rot am See +74582 Gerabronn +74589 Satteldorf +74592 Kirchberg an der Jagst +74599 Wallhausen +97980 Bad Mergentheim +97999 Igersheim +74677 Dörzbach +97993 Creglingen +97896 Freudenberg, Collenberg +97900 Külsheim +63928 Eichenbühl +74746 Höpfingen +97941 Tauberbischofsheim +97956 Werbach +97953 Königheim +97957 Wittighausen +97947 Grünsfeld +97950 Großrinderfeld +73441 Bopfingen +73450 Neresheim +73485 Unterschneidheim +73495 Stödtlen +89176 Asselfingen +89192 Rammingen +89568 Hermaringen +89537 Giengen an der Brenz +89168 Niederstotzingen +89522 Heidenheim an der Brenz +89542 Herbrechtingen +89567 Sontheim an der Brenz +89561 Dischingen +89518 Heidenheim an der Brenz +73432 Aalen +89520 Heidenheim an der Brenz +89564 Nattheim +73463 Westhausen +73466 Lauchheim +73469 Riesbürg +73467 Kirchheim am Ries +73492 Rainau +73488 Ellenberg +73499 Wört +74579 Fichtenau +73497 Tannhausen +74594 Kreßberg +87538 Fischen im Allgäu +88145 Hergatz +87534 Oberstaufen +87452 Altusried +88142 Wasserburg (Bodensee) +88131 Lindau (Bodensee) +88149 Nonnenhorn +88138 Sigmarszell +88178 Heimenkirch +88175 Scheidegg +88167 Röthenbach (Allgäu) +88179 Oberreute +88171 Weiler-Simmerberg +87547 Missen-Wilhams +88161 Lindenberg im Allgäu +89257 Illertissen +89231 Neu-Ulm +89275 Elchingen +89250 Senden +89269 Vöhringen +89233 Neu-Ulm +89081 Ulm +63924 Kleinheubach, Rüdenau +63920 Großheubach +63916 Amorbach +63931 Kirchzell +63897 Miltenberg +63937 Weilbach +63856 Bessenbach +63762 Großostheim +63820 Elsenfeld +63743 Aschaffenburg +63849 Leidersbach +63853 Mömlingen +63785 Obernburg a.Main +63843 Niedernberg +63868 Großwallstadt +63834 Sulzbach am Main +63839 Kleinwallstadt +63840 Hausen +63933 Mönchberg +63906 Erlenbach a.Main +63939 Wörth a.Main +63934 Röllbach +63911 Klingenberg a. Main +63925 Laudenbach +97440 Werneck +97840 Hafenlohr, Rothenbuch +97845 Neustadt a. Main +97753 Karlstadt +97836 Bischbrunn +74731 Walldürn +97285 Röttingen, Tauberrettersheim +97243 Bieberehren +97896 Freudenberg, Collenberg +63936 Schneeberg +63928 Eichenbühl +63927 Bürgstadt +63930 Neunkirchen +97277 Neubrunn +63874 Dammbach +63872 Heimbuchenthal +97909 Stadtprozelten +97901 Altenbuch +97906 Faulbach +97852 Schollbrunn +63860 Rothenbuch, Rothenbucher Forst +63879 Weibersbrunn, Rohrbrunner Forst +63857 Waldaschaff, Waldaschaffer Forst +63875 Mespelbrunn +97903 Collenberg +97904 Dorfprozelten +63863 Eschau +97855 Triefenstein +97839 Esselbach +97828 Marktheidenfeld +97892 Kreuzwertheim +97854 Steinfeld +97837 Erlenbach b. Marktheidenfeld +97907 Hasloch +97851 Rothenfels +97849 Roden +97842 Karbach +97292 Üttingen, Holzkirchen +97264 Helmstadt +97270 Kist, Irtenberger Wald +97234 Reichenberg, Guttenberger Wald +97244 Bütthard +97237 Altertheim +97268 Kirchheim +97256 Geroldshausen +97271 Kleinrinderfeld +97084 Würzburg +97318 Kitzingen +97199 Ochsenfurt +97252 Frickenhausen +97286 Winterhausen +97255 Gelchsheim, Sonderhofen +97246 Eibelstadt +97239 Aub +97283 Riedenheim +97232 Giebelstadt +97253 Gaukönigshofen +97289 Thüngen +97267 Himmelstadt +97857 Urspringen +97225 Zellingen +97282 Retzstadt +97834 Birkenfeld +97291 Thüngershem +97276 Margetshöchheim +97280 Remlingen +97259 Greußenheim +97274 Leinach +97265 Hettstadt +97297 Waldbüttelbrunn +97204 Höchberg +97295 Waldbrunn, Irtenberger Wald +97249 Eisingen +97299 Zell a. Main +97250 Erlabrunn +97076 Würzburg +97078 Würzburg +97080 Würzburg +97082 Würzburg +97337 Dettelbach +97241 Bergtheim, Oberpleichfeld +97222 Rimpar +97074 Würzburg +97262 Hausen b. Würzburg +97294 Unterpleichfeld +97230 Estenfeld +97273 Kürnach +97228 Rottendorf +97236 Randersacker +97288 Theilheim +97209 Veitshöchheim +97261 Güntersleben +97070 Würzburg +97072 Würzburg +97218 Gerbrunn +82467 Garmisch-Partenkirchen +87541 Bad Hindelang +87561 Oberstdorf +6993 Mittelberg +82481 Mittenwald +82475 Garmisch-Partenkirchen (Schneefernerhaus) +87509 Immenstadt im Allgäu +87544 Blaichach +87545 Burgberg im Allgäu +87549 Rettenberg +87448 Waltenhofen +87527 Sonthofen +87637 Seeg +86399 Bobingen +86473 Ziemetshausen +86947 Weil +86971 Peiting +86807 Buchloe +86929 Penzing +86853 Langerringen +87480 Weitnau +87477 Sulzberg +87642 Halblech +87645 Schwangau +87463 Dietmannsried +87754 Kammlach +87459 Pfronten +87497 Wertach +87484 Nesselwang +87466 Oy-Mittelberg +87439 Kempten (Allgäu) +87435 Kempten (Allgäu) +87487 Wiggensbach +87474 Buchenberg +87764 Legau +87437 Kempten (Allgäu) +87488 Betzigau +87634 Obergünzburg +87647 Unterthingau +87648 Aitrang +87493 Lauben +87490 Haldenwang +87496 Untrasried +87499 Wildpoldsried +87471 Durach +87657 Görisried +87629 Füssen +87659 Hopferau +87669 Rieden am Forggensee +82409 Wildsteig +87616 Marktoberdorf +87675 Stötten am Auerberg +87663 Lengenwang +87494 Rückholz +87672 Roßhaupten +87651 Bidingen +87640 Biessenhofen +87674 Ruderatshofen +86984 Prem +86987 Schwabsoien +86975 Bernbeuren +86980 Ingenried +86956 Schongau +86983 Lechbruck +86972 Altenstadt +86986 Schwabbruck +86989 Steingaden +86977 Burggen +87763 Lautrach +87749 Hawangen +87781 Ungerhausen +87734 Benningen +87760 Lachen +87700 Memmingen +87784 Westerheim +87752 Holzgünz +87766 Memmingerberg +87758 Kronburg +87730 Bad Grönenbach +87789 Woringen +87787 Wolfertschwenden +87736 Böhen +87724 Ottobeuren +87751 Heimertingen +87761 Lauben +87740 Buxheim +87779 Trunkelsberg +87742 Apfeltrach +87778 Stetten +87776 Sontheim +87719 Mindelheim +87733 Markt Rettenbach +87671 Ronsberg +87782 Unteregg +87653 Eggenthal +87654 Friesenried +87746 Erkheim +86488 Breitenthal +86498 Kettershausen +89299 Unterroth +89293 Kellmünz a.d. Iller +89287 Bellenberg +89297 Roggenburg +89296 Osterberg +89290 Buch +89281 Altenstadt +87767 Niederrieden +87737 Boos +87773 Pleß +87770 Oberschönegg +87755 Kirchhaslach +87727 Babenhausen +87743 Egg an der Günz +87748 Fellheim +87785 Winterrieden +89294 Oberroth +86381 Krumbach (Schwaben) +86489 Deisenhausen +86491 Ebershausen +86480 Aletshausen +86470 Thannhausen +86483 Balzhausen +86513 Ursberg +87745 Eppishausen +87757 Kirchheim in Schwaben +87775 Salgen +87739 Breitenbrunn +87772 Pfaffenhausen +87769 Oberrieden +86860 Jengen +86879 Wiedergeltingen +86842 Türkheim +86825 Bad Wörishofen +86869 Oberostendorf +87600 Kaufbeuren +87650 Baisweil +87660 Irsee +87666 Pforzen +87668 Rieden +87656 Germaringen +87677 Stöttwang +87665 Mauerstetten +87679 Westendorf +87662 Kaltental +86932 Pürgen +86978 Hohenfurch +86940 Schwifting +86859 Igling +86899 Landsberg a. Lech +86946 Vilgertshofen +86875 Waal +86920 Denklingen +86934 Reichling +86925 Fuchstal +86981 Kinsau +86944 Unterdießen +86863 Langenneufnach +86479 Aichen +86877 Walkertshofen +86868 Mittelneufnach +86856 Hiltenfingen +86850 Fischach +86872 Scherstetten +86865 Markt Wald +86845 Großaitingen +86833 Ettringen +86866 Mickhausen +86874 Tussenhausen +86854 Amberg +86830 Schwabmünchen +86871 Rammingen +86511 Schmiechen +86343 Königsbrunn +86862 Lamerdingen +86517 Wehringen +86507 Oberottmarshausen +86857 Hurlach +86836 Untermeitingen +86937 Scheuring +86931 Prittriching +86916 Kaufering +82490 Farchant +82442 Saulgrub +82438 Eschenlohe +82445 Schwaigen +82496 Oberau +82487 Oberammergau +82497 Unterammergau +82488 Ettal +82491 Grainau +85221 Dachau +85232 Bergkirchen +85764 Oberschleißheim +85452 Moosinning +82293 Mittelstetten +82362 Weilheim i. OB +82431 Kochel a. See +82541 Münsing +83661 Lenggries +83623 Dietramszell +83676 Jachenau +83708 Kreuth +83700 Rottach-Egern +82499 Wallgau +82444 Schlehdorf +82493 Krün +82494 Krün +82432 Walchensee +82441 Ohlstadt +82389 Böbing +82405 Wessobrunn +82398 Polling +82380 Peißenberg +82401 Rottenbuch +82436 Eglfing +82383 Hohenpeißenberg +82386 Huglfing +82433 Bad Kohlgrub +82435 Bayersoien +82449 Uffing a. Staffelsee +82387 Antdorf +82390 Eberfing +82392 Habach +82402 Seeshaupt +82393 Iffeldorf +82404 Sindelsdorf +82395 Obersöchering +82418 Murnau a. Staffelsee +82439 Großweil +82447 Spatzenhausen +82377 Penzberg +82549 Königsdorf +82547 Eurasburg +83671 Benediktbeuern +83673 Bichl +83670 Bad Heilbrunn +83646 Bad Tölz, Wackersberg +83677 Reichersbeuern +83674 Gaißach +83666 Waakirchen +83703 Gmund a. Tegernsee +83627 Warngau +83607 Holzkirchen +83684 Tegernsee +83707 Bad Wiessee +83679 Sachsenkam +86974 Apfeldorf +82407 Wielenbach +82399 Raisting +86928 Hofstetten +86949 Windach +86935 Rott +86919 Utting a. Ammersee +86911 Dießen a. Ammersee +86943 Thaining +86938 Schondorf a. Ammersee +86923 Finning +82343 Pöcking +82335 Berg +82211 Herrsching a. Ammersee +82234 Weßling +82229 Seefeld +82131 Gauting +82266 Inning a. Ammersee +82327 Tutzing +82346 Andechs +82319 Starnberg +82340 Feldafing +82396 Pähl +82347 Bernried +86415 Mering +86504 Merching +82297 Steindorf +82279 Eching a. Ammersee +82285 Hattenhofen +82288 Kottgeisering +82278 Althegnenberg +82272 Moorenweis +82276 Adelshofen +82287 Jesenwang +82269 Geltendorf +82299 Türkenfeld +86492 Egling a.d. Paar +86926 Greifenberg +86922 Eresing +86941 Eresing +82205 Gilching +82349 Pentenried +82152 Planegg/Krailling +82237 Wörthsee +82290 Landsberied +82140 Olching +82256 Fürstenfeldbruck +82284 Grafrath +82216 Maisach +82281 Egenhofen +82296 Schöngeising +82291 Mammendorf +82178 Puchheim +82110 Germering +82239 Alling +82275 Emmering +82294 Oberschweinbach +82223 Eichenau +82061 Neuried +82538 Geretsried +82057 Icking +82069 Schäftlarn +82067 Schäftlarn +82065 Baierbrunn +82544 Egling +82064 Straßlach-Dingharting +82515 Wolfratshausen +82031 Grünwald +82049 Pullach i. Isartal +85662 Hohenbrunn +85630 Grasbrunn +85635 Höhenkirchen-Siegertsbrunn +85653 Aying +85649 Brunnthal +85521 Ottobrunn/Riemerling +82024 Taufkirchen +82054 Sauerlach +82041 Oberhaching +83624 Otterfing +83626 Valley +85757 Karlsfeld +80639 München +80995 München +80997 München +80999 München +80638 München +80687 München +81243 München +81475 München +81241 München +81379 München +81245 München +81249 München +82194 Gröbenzell +82166 Gräfelfing +81479 München +81477 München +81476 München +80689 München +81375 München +81377 München +80686 München +80339 München +80634 München +81369 München +81373 München +81247 München +80993 München +80637 München +80992 München +81547 München +81545 München +81543 München +85551 Kirchheim b. München +85622 Feldkirchen +85737 Ismaning +85640 Putzbrunn +85579 Neubiberg +85774 Unterföhring +85609 Aschheim +85748 Garching b. München +80937 München +80805 München +80939 München +81827 München +81737 München +81925 München +81929 München +81829 München +81735 München +81549 München +81739 München +82008 Unterhaching +80335 München +80333 München +80469 München +80336 München +80337 München +80636 München +80538 München +80539 München +80331 München +81371 München +81541 München +81667 München +81539 München +81669 München +81679 München +81673 München +81677 München +81671 München +81675 München +81927 München +81825 München +85540 Haar +80935 München +80802 München +80799 München +80807 München +80796 München +80804 München +80801 München +80809 München +80797 München +80798 München +80803 München +80933 München +85665 Moosach +83727 Schliersee +83735 Bayrischzell +84405 Dorfen +84419 Schwindegg +83088 Kiefersfelden +83308 Trostberg +83324 Ruhpolding +83278 Traunstein +83730 Fischbachau +83737 Irschenberg +83714 Miesbach +83734 Hausham +83629 Weyarn +83075 Bad Feilnbach +83043 Bad Aibling +83115 Neubeuern +83059 Kolbermoor +83026 Rosenheim +83131 Nußdorf a. Inn +83126 Flintsbach a. Inn +83080 Oberaudorf +83122 Samerberg +83064 Raubling +83098 Brannenburg +83101 Rohrdorf +83229 Aschau i. Chiemgau +83233 Bernau a. Chiemsee +83209 Prien a. Chiemsee, Herrenchiemssee +83083 Riedering +83112 Frasdorf +83259 Schleching +83224 Grassau +83246 Unterwössen +83377 Vachendorf +83236 Übersee +83250 Marquartstein +83346 Bergen +83355 Grabenstätt +83242 Reit im Winkl +85625 Glonn +85667 Oberpframmern +85567 Grafing b. München +85658 Egmating +85617 Aßling +83052 Bruckmühl +83104 Tuntenhausen +83620 Feldkirchen-Westerham +83135 Schechen +83024 Rosenheim +83539 Pfaffing +83556 Griesstätt +83543 Rott a. Inn +83533 Edling +83553 Frauenneuharting +83569 Vogtareuth +83109 Großkarolinenfeld +83134 Prutting +83022 Rosenheim +83071 Stephanskirchen +83550 Emmering +83561 Ramerberg +85464 Finsing +85469 Walpertskirchen +85467 Neuching +85669 Pastetten +85457 Wörth +85659 Forstern +85570 Markt Schwaben +85604 Zorneding +85614 Kirchseeon +85586 Poing +85646 Anzing +85661 Forstinning +85598 Baldham +85591 Vaterstetten +85652 Pliening +85560 Ebersberg +85664 Hohenlinden +85599 Parsdorf/Hergolding +85656 Buch a. Buchrain +85643 Steinhöring +84435 Lengdorf +84424 Isen +84427 Sankt Wolfgang +83562 Rechtmehring +83558 Maitenbeth +83544 Albaching +83527 Haag i. OB +83129 Höslwang +83125 Eggstätt +83137 Schonstett +83139 Söchtenau +83093 Bad Endorf +83512 Wasserburg a. Inn +83549 Eiselfing +83253 Rimsting +83128 Halfing +83123 Amerang +83254 Breitbrunn a. Chiemsee +83530 Schnaitsee +83119 Obing +83132 Pittenhart +83257 Gstadt a. Chiemsee +83361 Kienberg +83256 Chiemsee +83365 Nußdorf +83301 Traunreut +83358 Seeon-Seebruck +83370 Seeon-Seebruck +83352 Altenmarkt a.d. Alz +83374 Traunreut +83371 Stein a.d. Traun +83376 Seeon-Seebruck +83339 Chieming +83368 St. Georgen +84544 Aschau a. Inn +84431 Heldenstein +84555 Jettenbach +84437 Reichertsheim +84539 Ampfing +83546 Gars am Inn +83567 Unterreit +83547 Babensham +83564 Soyen +83559 Gars a. Inn +83536 Gars a. Inn +83555 Gars a. Inn +84565 Oberneukirchen +84549 Engelsberg +84574 Taufkirchen +84453 Mühldorf a. Inn +84562 Mettenheim +84570 Polling +84513 Töging a. Inn +84478 Waldkraiburg +84559 Kraiburg a. Inn +83342 Tacherting +84579 Unterneukirchen +84550 Feichten a.d. Alz +84518 Garching a.d. Alz +84577 Tüßling +83486 Ramsau b. Berchtesgaden +84375 Kirchdorf a. Inn +83454 Anger +83317 Teisendorf +83313 Siegsdorf +84524 Neuötting +83471 Berchtesgaden & Schönau +83483 Bischofswiesen +83364 Neukirchen am Teisenberg +83458 Schneizlreuth +83334 Inzell +83404 Ainring +83435 Bad Reichenhall +83487 Marktschellenberg +83457 Bayerisch Gmain +83451 Piding +83395 Freilassing +83413 Fridolfing +83349 Palling +83373 Taching a. See +83329 Waging a. See +83362 Surberg +83417 Kirchanschöring +83367 Petting +83379 Wonneberg +84529 Tittmoning +83410 Laufen +83416 Saaldorf +84543 Winhöring +84508 Burgkirchen an der Alz +84553 Halsbach +84503 Altötting +84558 Kirchweidach +84556 Kastl +84489 Burghausen +84547 Emmerting +84561 Mehring +84576 Teising +84387 Julbach +84533 Marktl +89352 Ellzee +89367 Waldstetten +89347 Bubesheim +89340 Leipheim +89359 Kötz +89312 Günzburg +89346 Bibertal +89335 Ichenhausen +89278 Nersingen +89284 Pfaffenhofen a.d. Roth +89264 Weißenhorn +86444 Affing +86450 Altenmünster +89438 Holzheim +89423 Gundelfingen a.d. Donau +89407 Dillingen a.d. Donau +89364 Rettenbach +86660 Tapfheim +86609 Donauwörth +86641 Rain +86688 Marxheim +91710 Gunzenhausen +91781 Weißenburg i. Bay. +91785 Pleinfeld +91743 Unterschwaningen +91550 Dinkelsbühl +89291 Holzheim +86519 Wiesenbach +86476 Neuburg a.d. Kammel +86505 Münsterhausen +89356 Haldenwang +89343 Jettingen-Scheppach +89350 Dürrlauingen +89358 Kammeltal +89368 Winterbach +89365 Röfingen +89349 Burtenbach +89361 Landensberg +89331 Burgau +89447 Zöschingen +89428 Syrgenstein +89415 Lauingen (Donau) +89429 Bachhagel +89435 Finningen +89446 Ziertheim +89426 Wittislingen +89437 Haunsheim +89441 Medlingen +89344 Aislingen +89355 Gundremmingen +89362 Offingen +89431 Bächingen a.d. Brenz +89353 Glött +86477 Adelsried +86465 Welden +86441 Zusmarshausen +86494 Emersacker +86424 Dinkelscherben +86486 Bonstetten +86514 Ustersbach +86497 Horgau +86459 Gessertshausen +86500 Kutzenhausen +86508 Rehling +86179 Augsburg +86199 Augsburg +86161 Augsburg +86156 Augsburg +86169 Augsburg +86167 Augsburg +86482 Aystetten +86356 Neusäß +86368 Gersthofen +86391 Stadtbergen +86456 Gablingen +86462 Langweid a. Lech +86420 Diedorf +86157 Augsburg +86159 Augsburg +86152 Augsburg +86150 Augsburg +86153 Augsburg +86154 Augsburg +86502 Laugna +89443 Schwenningen +89440 Lutzingen +89420 Höchstädt a.d. Donau +89434 Blindheim +86637 Wertingen +86647 Buttenwiesen +86447 Aindling +86485 Biberbach +86405 Meitingen +86707 Westendorf, Kühlenthal +86663 Asbach-Bäumenheim +86690 Mertingen +86692 Münster +86678 Ehingen +86672 Thierhaupten +86695 Nordendorf +86679 Ellgau +86698 Oberndorf a.Lech +86739 Ederheim +86757 Wallerstein +86745 Hohenaltheim +86720 Nördlingen +86735 Amerdingen +86742 Fremdingen +86747 Maihingen +86748 Marktoffingen +91614 Mönchsroth +91725 Ehingen +91726 Gerolfingen +91749 Wittelshofen +91634 Wilburgstetten +91744 Weiltingen +91602 Dürrwangen +91731 Langfurth +86753 Möttingen +86733 Alerheim +86751 Mönchsdeggingen +86759 Wechingen +86738 Deiningen +86650 Wemding +86685 Huisheim +86657 Bissingen +86655 Harburg +86756 Reimlingen +86709 Wolferstadt +86700 Otting +86681 Fünfstetten +86653 Monheim +86687 Kaisheim +86694 Niederschönenfeld +86682 Genderkingen +91799 Langenaltheim +86675 Buchdorf +86736 Auhausen +86754 Munningen +86744 Hainsfarth +86732 Oettingen i. Bay. +86750 Megesheim +91747 Westheim +91805 Polsingen +91719 Heidenheim +91728 Gnotzheim +91740 Röckingen +91717 Wassertrüdingen +86741 Ehingen a. Ries +91757 Treuchtlingen +91801 Markt Berolzheim +91723 Dittenheim +91792 Ellingen +91793 Alesheim +91802 Meinheim +91741 Theilenhofen +86551 Aichach +86568 Hollenbach +86577 Sielenbach +86510 Ried +86559 Adelzhausen +86438 Kissing +86495 Eurasburg +86316 Friedberg +86453 Dasing +85406 Zolling +85298 Scheyern +85229 Markt Indersdorf +85235 Odelzhausen +85110 Kipfenberg +85111 Adelschlag +84089 Aiglsbach +93309 Kelheim +93336 Altmannstein +92334 Berching +92363 Breitenbrunn +92345 Dietfurt a.d. Altmühl +86697 Oberhausen +86633 Neuburg an der Donau +91795 Dollnstein +91177 Thalmässing +86163 Augsburg +86165 Augsburg +86573 Obergriesbach +86576 Schiltberg +85302 Gerolsbach +85253 Erdweg +85247 Schwabhausen +85250 Altomünster +85259 Sulzemoos +85254 Sulzemoos +86567 Hilgertshausen-Tandern +86554 Pöttmes +86570 Inchenhofen +86574 Petersdorf +86674 Baar +86676 Ehekirchen +86666 Burgheim +86684 Holzheim +86556 Kühbach +86565 Gachenbach +86529 Schrobenhausen +86561 Aresing +86571 Langenmosen +86579 Waidhofen +86564 Brunnen +86562 Berg im Gau +86701 Rohrenfels +86669 Königsmoos +86668 Karlshuld +85777 Fahrenzhausen +85307 Paunzhausen +85411 Hohenkammer +85305 Jetzendorf +85293 Reichertshausen +85256 Vierkirchen +85258 Weichs +85238 Petershausen +85244 Röhrmoos +85778 Haimhausen +85241 Hebertshausen +85716 Unterschleißheim +85414 Kirchdorf a.d. Amper +85386 Eching +85376 Neufahrn b. Freising +85375 Neufahrn b. Freising +85391 Allershausen +85402 Kranzberg +85399 Hallbergmoos +85356 Freising +85354 Freising +85276 Pfaffenhofen a.d. Ilm +85309 Pörnbach +85296 Rohrbach +85107 Baar-Ebenhausen +85084 Reichertshofen +85304 Ilmmünster +85123 Karlskron +86558 Hohenwart +85395 Attenkirchen +85290 Geisenfeld +85301 Schweitenkirchen +85283 Wolnzach +84104 Rudelzhausen +84072 Au in der Hallertau +84048 Mainburg +86703 Rögling +86704 Tagmersheim +86643 Rennertshofen +91809 Wellheim +91804 Mörnsheim +91807 Solnhofen +85051 Ingolstadt +85049 Ingolstadt +85114 Buxheim +85128 Nassenfels +85117 Eitensheim +85122 Hitzhofen +85137 Walting +85072 Eichstätt +85116 Egweil +86706 Weichering +86673 Bergheim +85132 Schernfeld +91790 Nennslingen +91788 Pappenheim +91798 Höttingen +91180 Heideck +91796 Ettenstatt +85131 Pollenfeld +85125 Kinding +85135 Titting +91171 Greding +85077 Manching +85101 Lenting +85092 Kösching +85120 Hepberg +85134 Stammham +85098 Großmehring +85080 Gaimersheim +85139 Wettstetten +85055 Ingolstadt +85057 Ingolstadt +85053 Ingolstadt +85113 Böhmfeld +85088 Vohburg a.d. Donau +85126 Münchsmünster +85119 Ernsgaden +85129 Oberdolling +85104 Pförring +93349 Mindelstetten +93333 Neustadt a.d. Donau +85095 Denkendorf +92339 Beilngries +93339 Riedenburg +93155 Hemau +91625 Schnelldorf +91626 Schopfloch +91583 Schillingsfürst +91631 Wettringen +91601 Dombühl +91610 Insingen +91637 Wörnitz +96152 Burghaslach +96181 Rauhenebrach +91126 Schwabach +91564 Neuendettelsau +91555 Feuchtwangen +91522 Ansbach +91592 Buch a. Wald +91578 Leutershausen +91472 Ipsheim +91413 Neustadt a.d.Aisch +97355 Kleinlangheim +97320 Albertshofen +97215 Uffenheim +97346 Iphofen +97353 Wiesentheid +97348 Rödelsee +97483 Eltmann +91599 Dentlein a. Forst +91589 Aurach +91632 Wieseth +91596 Burk +91567 Herrieden +91572 Bechhofen +91620 Ohrenbach +91587 Adelshofen +91541 Rothenburg ob der Tauber +91607 Gebsattel +91608 Geslau +91635 Windelsbach +91628 Steinsfeld +91616 Neusitz +91605 Gallmersgarten +91593 Burgbernheim +91465 Ergersheim +91604 Flachslanden +91598 Colmberg +91611 Lehrberg +91617 Oberdachstetten +91471 Illesheim +91438 Bad Windsheim +91619 Obernzenn +91613 Marktbergel +91735 Muhr a. See +91722 Arberg +91737 Ornbau +91746 Weidenbach +91580 Petersaurach +91732 Merkendorf +91595 Burgoberbach +91586 Lichtenau +91623 Sachsen b. Ansbach +91639 Wolframs-Eschenbach +91183 Abenberg +91189 Rohr +91720 Absberg +91738 Pfofeld +91729 Haundorf +91174 Spalt +91575 Windsbach +91734 Mitteleschenbach +91629 Weihenzell +91622 Rügland +91590 Bruckberg +91452 Wilhermsdorf +91459 Markt Erlbach +91448 Emskirchen +90616 Neuhof a.d.Zenn +90619 Trautskirchen +90599 Dietenhofen +90768 Fürth +90587 Veitsbronn +90556 Cadolzburg +91560 Heilsbronn +90579 Langenzenn +90613 Großhabersdorf +90574 Roßtal +90513 Zirndorf +90614 Ammerndorf +91478 Markt Nordheim +97342 Obernbreit +97340 Marktbreit +97258 Ippesheim +97350 Mainbernheim +91443 Scheinfeld +91483 Oberscheinfeld +91474 Langenfeld +91463 Dietersheim +91477 Markt Bibart +91484 Sugenheim +97523 Schwanfeld +97509 Kolitzheim +97537 Wipfeld +97332 Volkach +97359 Schwarzach a. Main +97334 Sommerach +97279 Prosselsheim +97247 Eisenheim +96157 Ebrach +96160 Geiselwind +97516 Oberschwarzach +97511 Lülsfeld +97529 Sulzheim +97447 Gerolzhofen +97513 Michelau i. Steigerwald, Hundelshausen +97497 Dingolshausen +97357 Prichsenstadt +96193 Wachenroth +91486 Uehlfeld +91460 Baudenbach +91462 Dachsbach +91481 Münchsteinach +91466 Gerhardshofen +91480 Markt Taschendorf +91456 Diespeck +91468 Gutenstetten +91487 Vestenbergsgreuth +91489 Wilhelmsdorf +91469 Hagenbüchach +91475 Lonnerstadt +91315 Höchstadt a.d.Aisch +91325 Adelsdorf +91350 Gremsdorf +91341 Röttenbach +91093 Heßdorf +91074 Herzogenaurach +91091 Großenseebach +91086 Aurachtal +91085 Weisendorf +91097 Oberreichenbach +90617 Puschendorf +96138 Burgebrach +96154 Burgwindheim +96185 Schönbrunn i. Steigerwald +96132 Schlüsselfeld +96170 Lisberg +97514 Oberaurach +96050 Bamberg +96135 Stegaurach +96178 Pommersfelden +96194 Walsdorf +96172 Mühlhausen +96052 Bamberg +96163 Gundelsheim +96047 Bamberg +96158 Frensdorf +96173 Oberhaid +96191 Viereth-Trunstadt +96103 Hallstadt +96049 Bamberg +96175 Pettstadt +96120 Bischberg +90530 Wendelstein +91186 Büchenbach +90596 Schwanstetten +91187 Röttenbach +91166 Georgensgmünd +91154 Roth +92367 Pilsach +92318 Neumarkt i.d. OPf. +92277 Hohenburg +92249 Vilseck +92275 Hirschbach +92256 Hahnbach +92655 Grafenwöhr +95466 Weidenberg, Kirchenpingarten +95473 Creußen +95469 Speichersdorf +96142 Hollfeld +91320 Ebermannstadt +91257 Pegnitz +91278 Pottenstein +91362 Pretzfeld +90602 Pyrbaum +90518 Altdorf bei Nürnberg +92353 Postbauer-Heng +92342 Freystadt +91161 Hilpoltstein +90584 Allersberg +90482 Nürnberg +90480 Nürnberg +90491 Nürnberg +90411 Nürnberg +90489 Nürnberg +90419 Nürnberg +90431 Nürnberg +90427 Nürnberg +90471 Nürnberg +90455 Nürnberg +90451 Nürnberg +90453 Nürnberg +90763 Fürth +90562 Heroldsberg +90765 Fürth +90547 Stein +90522 Oberasbach +90469 Nürnberg +90449 Nürnberg +90766 Fürth +90762 Fürth +90429 Nürnberg +90439 Nürnberg +90441 Nürnberg +90443 Nürnberg +90402 Nürnberg +90403 Nürnberg +90478 Nürnberg +90459 Nürnberg +90461 Nürnberg +90425 Nürnberg +90409 Nürnberg +90408 Nürnberg +90475 Nürnberg +91227 Leinburg +91207 Lauf an der Pegnitz +90607 Rückersdorf +91242 Ottensoos +90552 Röthenbach an der Pegnitz +90571 Schwaig b. Nürnberg, Behringersdorfer Forst +90610 Winkelhaid +90537 Nürnberg-Feucht, Feuchter Forst +90559 Burgthann +90592 Schwarzenbruck +90473 Nürnberg +92364 Deining +92361 Berngau +92360 Mühlhausen +92369 Sengenthal +92358 Seubersdorf i.d. OPf. +92355 Velburg +92331 Parsberg +91238 Engelthal/Offenhausen +92348 Berg b.Neumarkt i.d.OPf. +92283 Lauterhofen +91230 Happurg +91224 Pommelsbrunn +91236 Alfeld +91244 Reichenschwand +91249 Weigendorf +91217 Hersbruck +91239 Henfenfeld +92262 Birgland +92237 Sulzbach-Rosenberg +92289 Ursensollen +92280 Kastl +92278 Illschwang +92260 Ammerthal +92259 Neukirchen b. Sulzbach-Rosen +91056 Erlangen +91058 Erlangen +91054 Erlangen +91080 Uttenreuth, Marloffstein +91096 Möhrendorf/Mark +91090 Effeltrich +91361 Pinzberg +91094 Langensendelbach +91353 Hausen +91352 Hallerndorf +91301 Forchheim +91077 Neunkirchen a. Brand +91099 Poxdorf +91336 Heroldsbach +91083 Baiersdorf +91088 Bubenreuth +91052 Erlangen +91334 Hemhofen +91358 Kunreuth +91322 Gräfenberg +91286 Obertrubach +91349 Egloffstein +91356 Kirchehrenbach +91327 Gößweinstein +91355 Hiltpoltstein +91338 Igensdorf +91359 Leutenbach +91233 Neunkirchen am Sand +90542 Eckental +91220 Schnaittach +91245 Simmelsdorf +91369 Wiesenthau +91367 Weißenohe +96110 Scheßlitz +96146 Altendorf +96123 Litzendorf +96114 Hirschaid +96167 Königsfeld +96129 Strullendorf +96117 Memmelsdorf +96155 Buttenheim +91365 Weilersbach +91330 Eggolsheim +91332 Heiligenstadt i. OFr. +95515 Plankenfels +91347 Aufseß +91346 Wiesenttal +91364 Unterleinleiter +91344 Waischenfeld +91235 Velden/Hartenstein +91287 Plech +91282 Betzenstein +91284 Neuhaus a.d.Pegnitz +91241 Kirchensittenbach +91247 Vorra +92265 Edelsfeld +92281 Königstein +92268 Etzelwang +92676 Eschenbach i.d. OPf. +91275 Auerbach i.d. OPf. +91281 Kirchenthumbach +95503 Hummeltal +95447 Bayreuth +95491 Ahorntal +95490 Mistelgau +95488 Eckersdorf +95496 Glashütten +95494 Gesees +95511 Mistelbach +95517 Emtmannsberg +95448 Bayreuth +95519 Vorbach +91289 Schnabelwaid +85368 Moosburg a.d. Isar +85416 Langenbach +85410 Haag a.d. Amper +85417 Marzling +85456 Wartenberg +85459 Berglern +85435 Erding +85462 Eitting +85461 Bockhorn +85447 Fraunberg +85465 Langenpreising +85445 Oberding +84061 Ergoldsbach +84056 Rottenburg a.d. Laaber +84095 Furth +84066 Mallersdorf-Pfaffenberg +84036 Landshut +84175 Gerzen +84144 Geisenhausen +84051 Essenbach +84183 Niederviehbach +84164 Moosthenning +84152 Mengkofen +84568 Pleiskirchen +94333 Geiselhöring +94315 Straubing +94419 Reisbach +94431 Pilsting +93077 Bad Abbach +93107 Thalmassing +93197 Zeitlarn +93086 Wörth an der Donau +93128 Regenstauf +93185 Michelsneukirchen +84186 Vilsheim +84169 Altfraunhofen +84174 Eching +84171 Baierbach +84172 Buch a. Erlbach +84184 Tiefenbach +84434 Kirchberg +84416 Taufkirchen (Vils) +84439 Steinkirchen +84432 Hohenpolding +85419 Mauern +85408 Gammelsdorf +85413 Hörgertshausen +85405 Nandlstadt +84091 Attenhofen +84106 Volkenschwand +84101 Obersüßbach +84076 Pfeffenhausen +84034 Landshut +84079 Bruckberg +84098 Hohenthann +84107 Weihmichl +84032 Landshut, Altdorf +84030 Ergolding, Landshut +84028 Landshut +84155 Bodenkirchen +84137 Vilsbiburg +84189 Wurmsham +84149 Velden +84428 Buchbach +84564 Oberbergkirchen +84181 Neufraunhofen +84494 Neumarkt-Sankt Veit +84546 Egglkofen +84573 Schönberg +84140 Gangkofen +84323 Massing +84103 Postau +84187 Weng +84166 Adlkofen +84100 Niederaichbach +84178 Kröning +84109 Wörth a.d. Isar +84168 Aham +84130 Dingolfing +84177 Gottfrieding +84160 Frontenhausen +84180 Loiching +84163 Marklkofen +94437 Mamming +84094 Elsendorf +93345 Hausen +93326 Abensberg +93358 Train +93352 Rohr i. NB +93342 Saal a.d. Donau +93354 Siegenburg +93359 Wildenberg +93348 Kirchdorf +84085 Langquaid +84097 Herrngiersdorf +84069 Schierling +84088 Neufahrn i. NB +93356 Teugn +93351 Painten +93346 Ihrlerstein +93343 Essing +93195 Wolfsegg +93180 Deuerling +93188 Pielenhofen +93164 Laaber, Brunn +93176 Beratzhausen +93152 Nittendorf +93161 Sinzing +93105 Tegernheim +93080 Pentling +93053 Regensburg +93059 Regensburg +93055 Regensburg +93173 Wenzenbach +93138 Lappersdorf +93096 Köfering +93051 Regensburg +93083 Obertraubling +93186 Pettendorf +93170 Bernhardswald +93057 Regensburg +93049 Regensburg +93047 Regensburg +84092 Bayerbach bei Ergoldsbach +84082 Laberweinting +93089 Aufhausen +93099 Mötzing +93104 Sünching +93101 Pfakofen +93095 Hagelstadt +94368 Perkam +94351 Feldkirchen +94330 Aiterhofen +94348 Atting +94369 Rain +94339 Leiblfing +93087 Alteglofsheim +93092 Barbing +93102 Pfatter +93177 Altenthann +93179 Brennberg +93093 Donaustauf +93098 Mintraching +93090 Bach an der Donau +93073 Neutraubling +93109 Wiesent +93192 Wald +94345 Aholfing +94350 Falkenfels +94344 Wiesenfelden +94365 Parkstetten +94356 Kirchroth +94377 Steinach +93191 Rettenbach +93167 Falkenstein +84332 Hebertsfelden +84335 Mitterskirchen +84329 Wurmannsquick +84552 Geratskirchen +84339 Unterdietfurt +84307 Eggenfelden +84326 Falkenberg +84571 Reischach +84567 Erlbach +84347 Pfarrkirchen +84337 Schönau +94327 Bogen +94428 Eichendorf +94081 Fürstenzell +94496 Ortenburg +94486 Osterhofen +94255 Böbrach +94259 Kirchberg +94227 Zwiesel +94253 Bischofsmais +94256 Drachselsried +84384 Wittibreut +84364 Bad Birnbach +84371 Triftern +84359 Simbach a. Inn +84389 Postmünster +84367 Tann +84333 Malgersdorf +94405 Landau a.d. Isar +94436 Simbach +94424 Arnstorf +84381 Johanniskirchen +84385 Egglham +84378 Dietersburg +94501 Aldersbach +94574 Wallerfing +94550 Künzing +94439 Roßbach +94149 Kößlarn +94148 Kirchham +94086 Griesbach i. Rottal +94094 Rotthalmünster +94140 Ering +94166 Stubenberg +94137 Bayerbach +94099 Ruhstorf a.d. Rott +94060 Pocking +94167 Tettenweis +94072 Bad Füssing +94152 Neuhaus a. Inn +94544 Hofkirchen +94474 Vilshofen an der Donau +94575 Windorf +94542 Haarbach +94161 Ruderting +94529 Aicha vorm Wald +94032 Passau +94127 Neuburg a. Inn +94154 Neukirchen vorm Wald +94034 Passau +94036 Passau +94113 Tiefenbach +94559 Niederwinkling +94342 Straßkirchen +94363 Oberschneiding +94553 Mariaposching +94522 Wallersdorf +94569 Stephansposching +94563 Otzing +94562 Oberpöring +94533 Buchhofen +94505 Bernried +94560 Offenberg +94539 Grafling +94554 Moos +94557 Niederalteich +94447 Plattling +94527 Aholming +94469 Deggendorf +94526 Metten +94353 Haibach +94374 Schwarzach +94366 Perasdorf +94359 Loitzendorf +94357 Konzell +94379 Sankt Englmar +94371 Rattenberg +94354 Haselbach +94375 Stallwang +94347 Ascha +94336 Hunderdorf +94360 Mitterfels +94372 Rattiszell +94362 Neukirchen +94267 Prackenbach +94250 Achslach +94262 Kollnburg +94239 Zachenberg +94265 Patersdorf +94234 Viechtach +94244 Teisnach +94535 Eging a. See +94577 Winzer +94508 Schöllnach +94532 Außernzell +94491 Hengersberg +94571 Schaufling +94551 Lalling +94541 Grattersdorf +94547 Iggensbach +94579 Zenting +94572 Schöfweg +94530 Auerbach +94104 Tittling +94538 Fürstenstein +94481 Grafenau +94169 Thurmansbang +94513 Schönberg +94536 Eppenschlag +94518 Spiegelau +94568 Sankt Oswald +94163 Saldenburg +94548 Innernzell +94157 Perlesreut +94261 Kirchdorf i. Wald +94264 Langdorf +94209 Regen +94249 Bodenmais +94269 Rinchnach +94252 Bayerisch Eisenstein +94566 Riedlhütte +94258 Frauenau +93133 Burglengenfeld +93183 Kallmünz +93182 Duggendorf +92366 Hohenfels +92287 Schmidmühlen +93149 Nittenau +93426 Roding +92253 Schnaittenbach +92263 Ebermannsdorf +92242 Hirschau +92286 Rieden +92421 Schwandorf +92539 Schönsee +92439 Bodenwöhr +92431 Neunburg vorm Wald +92445 Neukirchen-Balbini +95643 Tirschenreuth +95679 Waldershof +92714 Pleystein +92637 Weiden in der OPf., Theisseil +93142 Maxhütte-Haidhof +93158 Teublitz +92442 Wackersdorf +92449 Steinberg +92272 Freudenberg +92245 Kümmersbruck +92266 Ensdorf +92284 Poppenricht +92224 Amberg +92546 Schmidgaden +92533 Wernberg-Köblitz +92507 Nabburg +92548 Schwarzach b. Nabburg +92521 Schwarzenfeld +92551 Stulln +92269 Fensterbach +92536 Pfreimd +93189 Reichenbach +93194 Walderbach +93199 Zell +92436 Bruck i.d. OPf. +93483 Pösing +93491 Stamsried +93413 Cham +93489 Schorndorf +93455 Traitsching +93482 Pemfling +92444 Rötz +92555 Trausnitz +92552 Teunz +92543 Guteneck +92447 Schwarzhofen +92545 Niedermurach +92540 Altendorf +92542 Dieterskirchen +92723 Tännesberg +93488 Schönthal +93464 Tiefenbach +92526 Oberviechtach +92557 Weiding +92554 Thanstein +92559 Winklarn +92271 Freihung +92274 Gebenbach +92700 Kaltenbrunn +92690 Pressath +92665 Altenstadt a.d. Waldnaab +92708 Mantel +92694 Etzenricht +92720 Schwarzenbach +92702 Kohlberg +92711 Parkstein +92729 Weiherhammer +92712 Pirk +92660 Neustadt a.d. Waldnaab +92706 Luhe-Wildenau +92718 Schirmitz +95506 Kastl +95700 Neusorg +95505 Immenreuth +95508 Kulmain +95683 Ebnath +95478 Kemnath +92724 Trabitz +95514 Neustadt a. Kulm +95676 Wiesau +95689 Fuchsmühl +95704 Pullenreuth +95688 Friedenfels +92670 Windischeschenbach +92681 Erbendorf +92703 Krummennaab +92715 Püchersreuth +92717 Reuth b. Erbendorf +92699 Bechtsried +92709 Moosbach +92727 Waldthurn +92648 Vohenstrauß +92696 Flossenbürg +92697 Georgenberg +92685 Floß +92705 Leuchtenberg +92726 Waidhaus +92693 Eslarn +92721 Störnstein +95703 Plößberg +95685 Falkenberg +95671 Bärnau +95666 Mitterteich +95695 Mähring +93468 Miltach +93437 Furth i. Wald +93476 Blaibach +93497 Willmering +93466 Chamerau +93477 Gleißenberg +93499 Zandt +93473 Arnschwang +93494 Waffenbrunn +93495 Weiding +93486 Runding +93449 Waldmünchen +93479 Grafenwiesen +93453 Neukirchen b. Hl. Blut +93480 Hohenwarth +93444 Kötzting +93485 Rimbach +93474 Arrach +93471 Arnbruck +93458 Eschlkam +93492 Treffelstein +92549 Stadlern +93462 Lam +93470 Lohberg +63796 Kahl am Main +63791 Karlstein am Main +63811 Stockstadt am Main +63826 Geiselbach +63773 Goldbach +63825 Schöllkrippen, Blankenbach +63741 Aschaffenburg +63755 Alzenau +63814 Mainaschaff +63867 Johannesberg +63776 Mömbris +63829 Krombach +63768 Hösbach +63801 Kleinostheim +63739 Aschaffenburg +63808 Haibach +63864 Glattbach +63869 Heigenbrücken +63846 Laufach +97843 Neuhütten +97833 Frammersbach +97859 Wiesthal +63871 Heinrichsthal +63877 Sailauf +63831 Wiesen, Wiesener Forst +63828 Kleinkahl +97762 Hammelburg +97772 Wildflecken +97778 Fellen +97657 Sandberg +97618 Hohenroth +97647 Nordheim v.d. Rhön +97848 Rechtenbach +97775 Burgsinn +97816 Lohr a. Main +97846 Partenstein +97788 Neuendorf +97794 Rieneck +97737 Gemünden a. Main +97799 Zeitlofs +97791 Obersinn +97785 Mittelsinn +97773 Aura i. Sinngrund +97797 Wartmannsroth +97776 Eußenheim +97783 Karsbach +97780 Gössenheim +97782 Gräfendorf +97727 Fuchsstadt +97729 Ramsthal +97717 Euerdorf +97725 Elfershausen +97535 Wasserlosen +97502 Euerbach +97450 Arnstein +97789 Oberleichtersbach +97795 Schondra +97769 Bad Brückenau +97723 Oberthulba +97786 Motten +97792 Riedenberg +97705 Burkardroth +97779 Geroda +97688 Bad Kissingen +97708 Bad Bocklet +97659 Schönau a.d. Brend +97650 Fladungen +97656 Oberelsbach +97653 Bischofsheim a.d. Rhön +97714 Oerlenbach +97526 Sennfeld +97464 Niederwerrn +97506 Grafenrheinfeld +97534 Waigolshausen +97490 Poppenhausen +97422 Schweinfurt +97469 Gochsheim +97508 Grettstadt +97493 Bergrheinfeld +97525 Schwebheim +97456 Dittelbrunn +97424 Schweinfurt +97505 Geldersheim +97532 Üchtelhausen +97520 Röthlein +96106 Ebern +96176 Pfarrweisach +96250 Ebensfeld +97453 Schonungen +97486 Königsberg i. Bay. +97494 Bundorf +97461 Hofheim i. UFr. +97421 Schweinfurt +97499 Donnersdorf +97488 Stadtlauringen +97478 Knetzgau +97539 Wonfurt +97503 Gädheim +97437 Haßfurt +97519 Riedbach +97491 Aidhausen +97531 Theres +97720 Nüdlingen +97702 Münnerstadt +97711 Maßbach +97724 Burglauer +97616 Bad Neustadt an der Saale +97517 Rannungen +97631 Bad Königshofen i. Grabfeld +97633 Sulzfeld +96166 Kirchlauter +96151 Breitbrunn +97496 Burgpreppach +97500 Ebelsbach +97475 Zeil a. Main +97522 Sand a. Main +96188 Stettfeld +96274 Itzgrund +96184 Rentweinsdorf +96190 Untermerzbach +96182 Reckendorf +96148 Baunach +96179 Rattelsdorf +96169 Lauter +96149 Breitengüßbach +96164 Kemmern +96161 Gerach +96126 Maroldsweisach +97528 Sulzdorf a.d. Lederhecke +96482 Ahorn +96486 Lautertal +96450 Coburg +96479 Weitramsdorf +96145 Seßlach +96484 Meeder +96476 Rodach b. Coburg +96269 Großheirath +97640 Oberstreu +97645 Ostheim v.d. Rhön +97638 Mellrichstadt +97654 Bastheim +96215 Lichtenfels +96231 Staffelstein +96199 Zapfendorf +96196 Wattendorf +07926 Gefell +95236 Stammbach +95369 Untersteinach +95326 Kulmbach +95336 Mainleus +96337 Ludwigsstadt +96369 Weißenbrunn +96224 Burgkunstadt +96187 Stadelhofen +95359 Kasendorf +96197 Wonsees +96260 Weismain +96272 Hochstadt a. Main +96264 Altenkunstadt +96465 Neustadt b. Coburg +96242 Sonnefeld +96253 Untersiemau +96237 Ebersdorf b. Coburg +96271 Grub a. Forst +96472 Rödental +96279 Weidhausen b. Coburg +96247 Michelau i. OFr. +96489 Niederfüllbach +96487 Dörfles-Esbach +96332 Pressig +96317 Kronach +96268 Mitwitz +96328 Küps +96342 Stockheim +96277 Schneckenlohe +96275 Marktzeuln +96257 Redwitz a.d. Rodach +95367 Trebgast +95361 Ködnitz +95339 Neuenmarkt +95512 Neudrossenfeld +95349 Thurnau +95445 Bayreuth +95500 Heinersreuth +95463 Bindlach +95444 Bayreuth +95239 Zell +95352 Marktleugast +95502 Himmelkron +95358 Guttenberg +95509 Marktschorgast +95362 Kupferberg +95499 Harsdorf +95493 Bischofsgrün +95497 Goldkronach +95482 Gefrees +95460 Bad Berneck im Fichtelgebirge +95485 Warmensteinach +95364 Ludwigschorgast +95179 Geroldsgrün +95346 Stadtsteinach +95365 Rugendorf +95355 Presseck +96365 Nordhalben +96352 Wilhelmsthal +96346 Wallenfels +96364 Marktrodach +96349 Steinwiesen +96358 Teuschnitz +95131 Schwarzenbach a. Wald +95188 Issigau +95152 Selbitz +95197 Schauenstein +95180 Berg +95233 Helmbrechts +95213 Münchberg +95119 Naila +95138 Bad Steben +95356 Grafengehaig +96361 Steinbach a. Wald +96355 Tettau +96367 Tschirn +95192 Lichtenberg +95682 Brand +95168 Marktleuthen +95709 Tröstau +95163 Weißenstadt +95195 Röslau +95158 Kirchenlamitz +95632 Wunsiedel +95234 Sparneck +95694 Mehlmeisel +95686 Fichtelberg +95697 Nagel +95701 Pechbrunn +95691 Hohenberg a.d. Eger +95199 Thierstein +95707 Thiersheim +95186 Höchstädt +95680 Bad Alexandersbad +95100 Selb +95659 Arzberg +95615 Marktredwitz +95189 Köditz +95032 Hof +95237 Weißdorf +95145 Oberkotzau +95182 Döhlau +95191 Leupoldsgrün +95030 Hof +95126 Schwarzenbach a.d. Saale +95176 Konradsreuth +95185 Gattendorf +95183 Feilitzsch +95028 Hof +95173 Schönwald +95194 Regnitzlosau +95111 Rehau +95692 Konnersreuth +95698 Neualbenreuth +95652 Waldsassen +95706 Schirnding +94116 Hutthurm +94139 Breitenberg +94107 Untergriesbach +94136 Thyrnau +94051 Hauzenberg +94121 Salzweg +94124 Büchlberg +94130 Obernzell +94164 Sonnen +94110 Wegscheid +94078 Freyung +94556 Neuschönau +94142 Fürsteneck +94545 Hohenau +94133 Röhrnbach +94146 Hinterschmiding +94151 Mauth +94065 Waldkirchen +94143 Grainet +94160 Ringelai +94118 Jandelsbrunn +94145 Haidmühle +94089 Neureichenau +94158 Philippsreut +28357 Bremen +28197 Bremen +28816 Stuhr +28259 Bremen +28195 Bremen +28199 Bremen +28201 Bremen +28277 Bremen +28779 Bremen +28777 Bremen +28755 Bremen +28237 Bremen +28719 Bremen +28217 Bremen +28239 Bremen +28759 Bremen +28757 Bremen +28717 Bremen +28215 Bremen +28219 Bremen +28307 Bremen +28211 Bremen +28309 Bremen +28329 Bremen +28327 Bremen +28876 Oyten +28279 Bremen +28203 Bremen +28207 Bremen +28205 Bremen +28325 Bremen +28209 Bremen +28213 Bremen +28355 Bremen +28359 Bremen +27572 Bremerhaven +27570 Bremerhaven +27578 Bremerhaven +27568 Bremerhaven, Bremen +27580 Bremerhaven +27576 Bremerhaven +27574 Bremerhaven +68623 Lampertheim +68647 Biblis +64589 Stockstadt am Rhein +65468 Trebur +64560 Riedstadt +64839 Münster +64385 Reichelsheim (Odenwald) +69239 Neckarsteinach +69483 Wald-Michelbach +69434 Hirschhorn, Brombach, Heddesbach +69151 Neckargemünd +69412 Eberbach +64760 Oberzent +64757 Unter-Hainbrunn +68519 Viernheim +68642 Bürstadt +68649 Groß-Rohrheim +64579 Gernsheim +64625 Bensheim +64646 Heppenheim (Bergstraße) +64683 Einhausen +64653 Lorsch +64673 Zwingenberg +69517 Gorxheimertal +69509 Mörlenbach +69488 Birkenau +64658 Fürth +64678 Lindenfels +64686 Lautertal (Odenwald) +64668 Rimbach +64689 Grasellenbach +69518 Abtsteinach +64319 Pfungstadt +64521 Groß-Gerau +64665 Alsbach-Hähnlein +64584 Biebesheim am Rhein +64404 Bickenbach +64331 Weiterstadt +64572 Büttelborn +64295 Darmstadt +64347 Griesheim +64289 Darmstadt +64293 Darmstadt +64397 Modautal +64846 Groß-Zimmern +64380 Roßdorf +64342 Seeheim-Jugenheim +64297 Darmstadt +64287 Darmstadt +64354 Reinheim +64409 Messel +64291 Darmstadt +64372 Ober-Ramstadt +64285 Darmstadt +64401 Groß-Bieberau +64807 Dieburg +64407 Fränkisch-Crumbach +64367 Mühltal +64405 Fischbachtal +64283 Darmstadt +64711 Erbach +64732 Bad König +64753 Brombachtal +64720 Michelstadt +64756 Mossautal +64853 Otzberg +64823 Groß-Umstadt +64850 Schaafheim +64747 Breuberg +64739 Höchst i. Odw. +64395 Brensbach +64750 Lützelbach +64832 Babenhausen +35690 Dillenburg +65510 Hünstetten, Idstein +61279 Grävenwiesbach +35708 Haiger +35753 Greifenstein +35781 Weilburg +65606 Villmar +65391 Lorch +65375 Oestrich-Winkel +65366 Geisenheim +65321 Heidenrod +65385 Rüdesheim am Rhein +65388 Schlangenbad +65307 Bad Schwalbach +65582 Diez, Hambach, Aull +65329 Hohenstein +65347 Eltville am Rhein +65344 Eltville am Rhein +65399 Kiedrich +65346 Eltville am Rhein +65232 Taunusstein +65343 Eltville am Rhein +65396 Walluf +65345 Eltville am Rhein +65195 Wiesbaden +65201 Wiesbaden +65199 Wiesbaden +65197 Wiesbaden +65428 Rüsselsheim +65474 Bischofsheim +65462 Ginsheim-Gustavsburg +65439 Flörsheim am Main +65817 Eppstein +65239 Hochheim am Main +65719 Hofheim am Taunus +65779 Kelkheim +65191 Wiesbaden +65183 Wiesbaden +65205 Wiesbaden +65187 Wiesbaden +65207 Wiesbaden +65193 Wiesbaden +65203 Wiesbaden +55246 Wiesbaden +55252 Mainz-Kastel +65185 Wiesbaden +65189 Wiesbaden +65326 Aarbergen +65597 Hünfelden +65611 Brechen +65529 Waldems +65527 Niedernhausen +65520 Bad Camberg +61479 Glashütten +61276 Weilrod +65618 Selters +65604 Elz +65554 Limburg +65556 Limburg +65550 Limburg +35794 Mengerskirchen +65589 Hadamar +65594 Runkel +65599 Dornburg +65614 Beselich +65620 Waldbrunn +35799 Merenberg +65549 Limburg +65553 Limburg +65555 Limburg +65551 Limburg +65552 Limburg +65627 Elbtal +35606 Solms +35619 Braunfels +35638 Leun +35789 Weilmünster +35792 Löhnberg +35796 Weinbach +35759 Driedorf +35767 Breitscheid +35683 Dillenburg +35688 Dillenburg +35684 Dillenburg +35687 Dillenburg +35630 Ehringshausen +35745 Herborn +35756 Mittenaar +35764 Sinn +35768 Siegbach +35686 Dillenburg +35685 Dillenburg +35689 Dillenburg +35236 Breidenbach +57319 Bad Berleburg +35713 Eschenburg +35716 Dietzhölztal +65451 Kelsterbach +65479 Raunheim +65812 Bad Soden am Taunus +65795 Hattersheim +65824 Schwalbach am Taunus +65931 Frankfurt am Main +65929 Frankfurt am Main +65760 Eschborn +60549 Frankfurt am Main +60488 Frankfurt am Main +60529 Frankfurt am Main +64569 Nauheim +64546 Mörfelden-Walldorf +64390 Erzhausen +63263 Neu-Isenburg +61239 Ober-Mörlen +61191 Rosbach v.d. Höhe +35287 Amöneburg +35327 Ulrichstein +36304 Alsfeld +36320 Kirtorf +35305 Grünberg +35321 Laubach +35410 Hungen +35447 Reiskirchen +35510 Butzbach +61250 Usingen +63654 Büdingen +63691 Ranstadt +63607 Wächtersbach +65830 Kriftel +65835 Liederbach am Taunus +65843 Sulzbach (Taunus) +65936 Frankfurt am Main +65934 Frankfurt am Main +65933 Frankfurt am Main +60489 Frankfurt am Main +63065 Offenbach am Main +63069 Offenbach am Main +63073 Offenbach am Main +63071 Offenbach am Main +60598 Frankfurt am Main +60435 Frankfurt am Main +60431 Frankfurt am Main +60386 Frankfurt am Main +60388 Frankfurt am Main +60528 Frankfurt am Main +60389 Frankfurt am Main +60314 Frankfurt am Main +60599 Frankfurt am Main +64859 Eppertshausen +63477 Maintal +63165 Mühlheim am Main +63329 Egelsbach +63225 Langen +63303 Dreieich +63150 Heusenstamm +63128 Dietzenbach +63322 Rödermark +60325 Frankfurt am Main +60326 Frankfurt am Main +60487 Frankfurt am Main +60486 Frankfurt am Main +60327 Frankfurt am Main +60323 Frankfurt am Main +60316 Frankfurt am Main +60322 Frankfurt am Main +60329 Frankfurt am Main +60594 Frankfurt am Main +60313 Frankfurt am Main +60311 Frankfurt am Main +60318 Frankfurt am Main +60385 Frankfurt am Main +60596 Frankfurt am Main +60320 Frankfurt am Main +60308 Frankfurt +60310 Frankfurt am Main (Taunusturm) +60306 Frankfurt am Main, Opernturm +63067 Offenbach am Main +63075 Offenbach am Main +61267 Neu-Anspach +61462 Königstein im Taunus +61440 Oberursel (Taunus) +61389 Schmitten +61476 Kronberg im Taunus +61273 Wehrheim +61350 Bad Homburg v.d. Höhe +61381 Friedrichsdorf +60439 Frankfurt am Main +61449 Steinbach (Taunus) +61348 Bad Homburg v.d. Höhe +60438 Frankfurt am Main +61203 Reichelsheim (Wetterau) +61184 Karben +61206 Wöllstadt +61231 Bad Nauheim +61138 Niederdorfelden +61352 Bad Homburg v.d. Höhe +61118 Bad Vilbel +61194 Niddatal +61169 Friedberg (Hessen) +61137 Schöneck +60437 Frankfurt am Main +60433 Frankfurt am Main +63452 Hanau +63456 Hanau +63454 Hanau +63457 Hanau +63110 Rodgau +63179 Obertshausen +63517 Rodenbach +63538 Großkrotzenburg +63512 Hainburg +63500 Seligenstadt +63526 Erlensee +63533 Mainhausen +63450 Hanau +63579 Freigericht +63776 Mömbris +61130 Nidderau +61197 Florstadt +63674 Altenstadt +63683 Ortenberg +63695 Glauburg +63694 Limeshain +63486 Bruchköbel +63543 Neuberg +63546 Hammersbach +63505 Langenselbold +63549 Ronneburg +63699 Kefenrod +63571 Gelnhausen +63584 Gründau +63589 Linsengericht +63594 Hasselroth +35576 Wetzlar +35581 Wetzlar +35582 Wetzlar +35578 Wetzlar +35398 Gießen +35625 Hüttenberg +35633 Lahnau +35641 Schöffengrund +35428 Langgöns +35647 Waldolms +35580 Wetzlar +35579 Wetzlar +35394 Gießen +35392 Gießen +61200 Wölfersheim +35415 Pohlheim +35423 Lich +35440 Linden +35463 Fernwald +35516 Münzenberg +35519 Rockenberg +35584 Wetzlar +35585 Wetzlar +35614 Aßlar +35644 Hohenahr +35649 Bischoffen +35075 Gladenbach +35080 Bad Endbach +35102 Lohra +35444 Biebertal +35452 Heuchelheim +35586 Wetzlar +35583 Wetzlar Garbeinheim +35043 Marburg +35396 Gießen +35085 Ebsdorfergrund +35096 Weimar (Lahn) +35112 Fronhausen +35418 Buseck +35435 Wettenberg +35457 Lollar +35460 Staufenberg +35469 Allendorf +35390 Gießen +61209 Echzell +63667 Nidda +63679 Schotten +63697 Hirzenhain +63688 Gedern +35260 Stadtallendorf +35315 Homberg (Ohm) +35325 Mücke +35329 Gemünden (Felda) +35466 Rabenau +36325 Feldatal +36326 Antrifttal +36329 Romrod +63599 Biebergemünd +63637 Jossgrund +63639 Flörsbachtal +36115 Hilders, Ehrenberg +36088 Hünfeld +36132 Eiterfeld +36145 Hofbieber +36154 Hosenfeld +36100 Petersberg +36381 Schlüchtern +36272 Niederaula +63628 Bad Soden-Salmünster +36396 Steinau an der Straße +63619 Bad Orb +63636 Brachttal +63633 Birstein +36391 Sinntal +36355 Grebenhain +36358 Herbstein +36369 Lautertal +36399 Freiensteinau +36041 Fulda +36039 Fulda +36103 Flieden +36119 Neuhof +36137 Großenlüder +36148 Kalbach +36318 Schwalmtal +36323 Grebenau +36341 Lauterbach +36367 Wartenberg +36287 Breitenbach am Herzberg +36110 Schlitz +36151 Burghaun +36364 Bad Salzschlirf +36166 Haunetal +36043 Fulda +36093 Künzell +36124 Eichenzell +36129 Gersfeld +36157 Ebersburg +36160 Dipperz +36163 Poppenhausen +36037 Fulda +36167 Nüsttal +36169 Rasdorf +36142 Tann +35719 Angelburg +35216 Biedenkopf +35232 Dautphetal +35239 Steffenberg +35088 Battenberg +35116 Hatzfeld +35083 Wetter +35282 Rauschenberg +34537 Bad Wildungen +34549 Edertal +34508 Willingen (Upland) +34630 Gilserberg +34632 Jesberg +35041 Marburg +35091 Cölbe +35117 Münchhausen +35094 Lahntal +35099 Burgwald +35119 Rosenthal +35039 Marburg +35037 Marburg +59969 Bromskirchen, Hallenberg +35108 Allendorf +35066 Frankenberg +35104 Lichtenfels +35274 Kirchhain +35288 Wohratal +35285 Gemünden +35279 Neustadt +34599 Neuental +34613 Schwalmstadt +34628 Willingshausen +34513 Waldeck +34516 Vöhl +35110 Frankenau +35114 Haina +34560 Fritzlar +34582 Borken +34596 Bad Zwesten +34311 Naumburg +34497 Korbach +34519 Diemelsee +34431 Marsberg +34454 Bad Arolsen +34477 Twistetal +34471 Volkmarsen +34289 Zierenberg +34308 Bad Emstal +34466 Wolfhagen +34474 Diemelstadt +34396 Liebenau (Hessen) +34479 Breuna +34576 Homberg +34621 Frielendorf +34626 Neukirchen (Knüll) +34633 Ottrau +34637 Schrecksbach +34639 Schwarzenborn +36280 Oberaula +34369 Hofgeismar +37216 Witzenhausen, Gutsbezirk +36251 Bad Hersfeld, Ludwigsau +34281 Gudensberg +34593 Knüllwald +34323 Malsfeld +36199 Rotenburg an der Fulda +36211 Alheim +36214 Nentershausen +36205 Sontra +37235 Hessisch Lichtenau +37242 Bad Sooden-Allendorf +34298 Helsa +34320 Söhrewald +36275 Kirchheim (Hessen) +36286 Neuenstein (Hessen) +34305 Niedenstein +34587 Felsberg +34590 Wabern +34212 Melsungen +34286 Spangenberg +34302 Guxhagen +34326 Morschen +34327 Körle +36179 Bebra +36217 Ronshausen +36277 Schenklengsfeld +36282 Hauneck +36289 Friedewald +36208 Wildeck +36266 Heringen +36269 Philippsthal +36284 Hohenroda +36219 Cornberg +37290 Meißner +37284 Waldkappel +37269 Eschwege +37276 Meinhard +37287 Wehretal +37293 Herleshausen +37296 Ringgau +34128 Kassel +34131 Kassel +34132 Kassel +34134 Kassel +34270 Schauenburg +34295 Edermünde +34225 Baunatal +34292 Ahnatal +34379 Calden +34246 Vellmar +34314 Espenau +34317 Habichtswald +34130 Kassel +34123 Kassel +34121 Kassel +34233 Kassel, Fuldatal +34125 Kassel +34355 Staufenberg +34253 Lohfelden +34260 Kaufungen +34277 Fuldabrück +34266 Niestetal +34329 Nieste +34346 Hann. Münden, Gutsbezirk Reinhardswald +34119 Kassel +34117 Kassel +34127 Kassel +34388 Trendelburg +34393 Grebenstein +34399 Oberweser +34376 Immenhausen +34359 Reinhardshagen +37218 Witzenhausen +37215 Witzenhausen +37217 Witzenhausen +37213 Witzenhausen +37249 Neu-Eichenberg +37247 Großalmerode +37297 Berkatal +37214 Witzenhausen +34385 Bad Karlshafen +37194 Bodenfelde, Wahlsburg +37281 Wanfried +37299 Weißenborn +27499 Neuwerk +21037 Hamburg +21149 Hamburg +21147 Hamburg +21129 Hamburg +21075 Hamburg +21077 Hamburg +21079 Hamburg +21107 Hamburg +21109 Hamburg +22113 Hamburg, Oststeinbek +21073 Hamburg +22609 Hamburg +22547 Hamburg +22559 Hamburg +22869 Schenefeld +22587 Hamburg +22589 Hamburg +22549 Hamburg +20539 Hamburg +20457 Hamburg +20537 Hamburg +22397 Hamburg +22605 Hamburg +22763 Hamburg +22335 Hamburg +22399 Hamburg +22417 Hamburg +22419 Hamburg +22453 Hamburg +22457 Hamburg +22459 Hamburg +22043 Hamburg +22111 Hamburg +22179 Hamburg +22391 Hamburg +22765 Hamburg +22767 Hamburg +22529 Hamburg +22527 Hamburg +22523 Hamburg +22307 Hamburg +22041 Hamburg +20359 Hamburg +22761 Hamburg +22607 Hamburg +22525 Hamburg +20357 Hamburg +20253 Hamburg +22769 Hamburg +20255 Hamburg +20259 Hamburg +20257 Hamburg +20095 Hamburg +20459 Hamburg +20097 Hamburg +20148 Hamburg +20146 Hamburg +20149 Hamburg +20144 Hamburg +20099 Hamburg +20354 Hamburg +20355 Hamburg +20249 Hamburg +20251 Hamburg +22087 Hamburg +22085 Hamburg +22301 Hamburg +22299 Hamburg +22303 Hamburg +22297 Hamburg +20535 Hamburg +22177 Hamburg +22089 Hamburg +22083 Hamburg +22081 Hamburg +22305 Hamburg +22049 Hamburg +22455 Hamburg +22415 Hamburg +22337 Hamburg +22339 Hamburg +22309 Hamburg +21031 Hamburg +21033 Hamburg +21035 Hamburg +21039 Hamburg +21029 Hamburg +22359 Hamburg +22143 Hamburg +22145 Hamburg +22159 Hamburg +22117 Hamburg +22119 Hamburg +22393 Hamburg +22115 Hamburg +22045 Hamburg +22047 Hamburg +22147 Hamburg +22149 Hamburg +22175 Hamburg +22395 Hamburg +19273 Amt Neuhaus, Stapel +19372 Spornitz +19294 Neu Kaliß +19288 Ludwigslust +19300 Grabow u.a. +19249 Lübtheen +19303 Dömitz +19357 Karstädt, Dambeck, Klüß +17209 Wredenhagen +19376 Marnitz, Siggelkow +17258 Feldberger Seenlandschaft +17255 Wesenberg +17235 Neustrelitz +17237 Möllenbeck +17248 Rechlin +17252 Mirow +19217 Rehna, Carlow u.a. +19246 Zarrentin +19258 Boizenburg, Gresse, Greven u.a. +23942 Dassow +19260 Vellahn +23923 Schönberg +18230 Rerik, Bastorf, Biendorf +18236 Kröpelin, Carinerland +19079 Banzkow, Sukow +19374 Domsühl, Mestlin, Obere Warnow u.a. +19089 Crivitz, Friedrichsruhe u.a. +19069 Lübstorf +19417 Warin +19243 Wittenburg u.a. +19230 Hagenow u.a. +19306 Neustadt-Glewe +23936 Grevesmühlen, Stepenitztal, Upahl u.a. +23948 Klütz +23974 Neuburg-Steinhausen, Hornstorf +23996 Bad Kleinen u.a. +23966 Wismar, Groß Krankow u.a. +23970 Wismar +19077 Rastow +19205 Gadebusch +19209 Lützow +19057 Schwerin +19071 Brüsewitz +19075 Pampow +19073 Wittenförden u.a. +19370 Parchim +19086 Plate +19067 Leezen +19065 Pinnow +19061 Schwerin +19063 Schwerin +19055 Schwerin +19053 Schwerin +19059 Schwerin +19412 Brüel +23968 Barnekow, Gägelow u.a. +23946 Boltenhagen +23972 Dorf Mecklenburg, Lübow u.a. +23992 Neukloster +23999 Insel Poel +18233 Neubukow, Ravensberg u.a. +18225 Kühlungsborn +18195 Tessin, Grammow u.a. +18276 Reimershagen, Lohmen, Zehna, Hägerfelde u.a. +17179 Gnoien u.a. +18299 Laage, Wardow u.a. +18246 Bützow u.a. +17192 Waren/ Müritz +17207 Röbel/Müritz +17194 Grabowhöfe, Moltzow u.a. +17213 Malchow u.a. +18334 Bad Sülze +18337 Marlow +19395 Plau am See +19406 Sternberg +19399 Goldberg +19386 Lübz, Passow +18196 Dummerstorf +17214 Nossentiner Hütte +18279 Lalendorf, Langhagen +18292 Krakow, Dobbin-Linstow u.a. +17166 Dahmen, Groß Wokern, Teterow +18249 Bernitt, Qualitz, Warnow, Zernin u.a. +18258 Schwaan u.a. +18273 Güstrow +18211 Retschow, Admannshagen-Bargeshagen u.a. +18209 Bad Doberan, Bartenshagen-Parkentin u.a. +18239 Satow +18198 Stäbelow, Kritzmow +18069 Rostock, Lambrechtshagen +18106 Rostock +18059 Rostock, Papendorf u.a. +18055 Rostock +18147 Rostock +18146 Rostock +18057 Rostock +17168 Jördenstorf, Prebberede u.a. +18184 Roggentin, Broderstorf u.a. +18182 Rostock, Gelbensande, Rövershagen u.a. +18190 Sanitz +17348 Woldegk +17099 Friedland, Galenbeck, Datzetal +17039 Sponholz, Neunkirchen u.a. +17349 Groß Miltzow +17098 Friedland +17094 Cölpin +17121 Loitz +17153 Stavenhagen +17091 Rosenow +17139 Malchin +17089 Burow +17129 Bentzin +17111 Demmin u.a. +17391 Krien u.a. +17495 Karlsburg +17392 Spantekow +18513 Glewitz +18516 Rakow +18519 Miltzow u.a. +18507 Grimmen +18461 Franzburg, Richtenberg u.a. +17219 Möllenhagen +17217 Penzlin +17033 Neubrandenburg +17087 Altentreptow +17036 Neubrandenburg +17034 Neubrandenburg +17159 Dargun +17154 Neukalen +17109 Demmin +18465 Tribsees +17126 Jarmen +17506 Gützkow +17489 Greifswald +17491 Greifswald +17493 Greifswald +17498 Neuenkirchen +18311 Ribnitz-Damgarten +18356 Barth +18107 Elmenhorst/Lichtenhagen, Rostock +18119 Rostock +18109 Rostock +18181 Rostock, Graal-Müritz +18347 Dierhagen +18320 Ahrenshagen-Daskow, Trinwillershagen u.a. +18317 Saal +18375 Prerow a. Darß +18314 Löbnitz +18469 Velgast +18556 Dranske +18528 Bergen/ Rügen +18581 Putbus +18574 Garz/ Rügen +18569 Gingst +18573 Samtens +18551 Sagard +18442 Niepars +18445 Prohn +18437 Stralsund +18510 Wittenhagen +18374 Zingst a. Darß +18435 Stralsund +18439 Stralsund +18565 Hiddensee +17328 Penkun u.a. +17440 Kröslin, Krummin, Lassan u.a. +17406 Usedom u.a. +17373 Ueckermünde +17321 Löcknitz, Rothenklempenow +17375 Vogelsang-Warsin, Meiersberg, Mönkebude u.a. +17322 Blankensee, Grambow u.a. +17367 Eggesin +17309 Pasewalk u.a. +17337 Uckerland, Groß Luckow, Schönhausen +17390 Klein Bünzow +17398 Ducherow +17509 Lubmin +17335 Strasburg +17379 Ferdinandshof u.a. +17329 Krackow, Nadrensee +17358 Torgelow +17389 Anklam +17449 Karlshagen +17438 Wolgast +17429 Benz, Heringsdorf u.a. +17419 Seebad Ahlbeck +17454 Zinnowitz +17459 Koserow +17424 Ostseebad Heringsdorf +18586 Sellin +18609 Binz +18546 Sassnitz +49847 Itterbeck/Wielen +49824 Ringe, Laar, Emlichheim +26757 Borkum +37127 Dransfeld u.a. +37170 Uslar +37176 Nörten-Hardenberg +37181 Hardegsen +37120 Bovenden +37133 Friedland +34355 Staufenberg +34346 Hann. Münden, Gutsbezirk Reinhardswald +37079 Göttingen +37124 Rosdorf +37139 Adelebsen +37085 Göttingen +37077 Göttingen +37075 Göttingen +37081 Göttingen +37083 Göttingen +37136 Seulingen, Waake u.a. +37130 Gleichen +37073 Göttingen +48527 Nordhorn +49811 Lingen +48465 Engden, Isterberg, Schüttorf u.a. +49828 Esche, Georgsdorf, Lage, Neuenhaus, Osterwald +49843 Uelsen, Halle, Gölenkamp, Getelo +48529 Nordhorn +48531 Nordhorn +49835 Wietmarschen +48455 Bad Bentheim +48488 Emsbüren +48499 Salzbergen +48480 Lünne, Schapen, Spelle +49832 Andervenne, Beesten, Freren, Messingen, Thuine +49328 Melle +49090 Osnabrück +49086 Osnabrück +49586 Merzen, Neuenkirchen +49143 Bissendorf +49163 Bohmte +49179 Ostercappeln +49134 Wallenhorst +49565 Bramsche +49434 Neuenkirchen-Vörden +49078 Osnabrück +49170 Hagen am Teutoburger Wald +49205 Hasbergen +49219 Glandorf +49599 Voltlage +49076 Osnabrück +49082 Osnabrück +49124 Georgsmarienhütte +49176 Hilter +49186 Bad Iburg +49196 Bad Laer +49201 Dissen +49214 Bad Rothenfelde +49326 Melle +49324 Melle +49080 Osnabrück +49088 Osnabrück +49191 Belm +49084 Osnabrück +49074 Osnabrück +49448 Lemförde u.a. +49152 Bad Essen +49846 Hoogstede +49849 Wilsum +26835 Hesel, Neukamperfehn u.a. +26897 Esterwegen u.a. +49777 Stavern +49744 Geeste +49733 Haren +49767 Twist +49751 Sögel u.a. +49762 Sustrum, Lathen u.a. +49779 Oberlangen, Niederlangen +26802 Moormerland +26817 Rhauderfehn +26826 Weener +26847 Detern +26871 Papenburg +49774 Lähden +49809 Lingen +49808 Lingen +49716 Meppen +49740 Haselünne +49838 Lengerich u.a. +49770 Herzlake, Dohren +49844 Bawinkel +26892 Dörpen, Lehe u.a. +26906 Dersum +26907 Walchum +26899 Rhede (Ems) +26909 Neubörger, Neulehe +26903 Surwold +26904 Börger +26831 Bunde +26844 Jemgum +26789 Leer +26810 Westoverledingen +26845 Nortmoor +49626 Berge, Bippen +49584 Fürstenau +49457 Drebber +49577 Kettenkamp, Eggermühlen, Ankum +49757 Werlte, Vrees, Lahn +26939 Ovelgönne +26160 Bad Zwischenahn +26169 Friesoythe +26180 Rastede +26188 Edewecht +26655 Westerstede +26689 Apen +26931 Elsfleth +26670 Uplengen +27793 Wildeshausen +27804 Berne +27798 Hude (Oldenburg) +27801 Dötlingen +26676 Barßel +26683 Saterland +26842 Ostrhauderfehn +49593 Bersenbrück +49393 Lohne +49624 Löningen +49637 Menslage +49401 Damme +49439 Steinfeld +49635 Badbergen +49661 Cloppenburg +49681 Garrel +49685 Emstek +49696 Molbergen +49594 Alfhausen +49638 Nortrup +49699 Lindern (Oldenburg) +49632 Essen (Oldenburg) +49688 Lastrup +49610 Quakenbrück +49597 Rieste +49451 Holdorf +49596 Gehrde +49413 Dinklage +49356 Diepholz +49459 Lembruch, Burlage +49456 Bakum +49692 Cappeln (Oldenburg) +49377 Vechta +49424 Goldenstedt +49429 Visbek +26901 Lorup, Rastdorf +26219 Bösel +26849 Filsum +26197 Großenkneten +26203 Wardenburg +26209 Hatten +26131 Oldenburg (Oldenburg) +26129 Oldenburg (Oldenburg) +26127 Oldenburg (Oldenburg) +26215 Wiefelstede +26133 Oldenburg (Oldenburg) +26135 Oldenburg (Oldenburg) +26125 Oldenburg (Oldenburg) +26123 Oldenburg (Oldenburg) +26122 Oldenburg +26121 Oldenburg (Oldenburg) +31737 Rinteln +31855 Aerzen +31840 Hessisch Oldendorf +31812 Bad Pyrmont +32361 Preußisch Oldendorf +31606 Warmsen +31603 Diepenau +31675 Bückeburg +31683 Obernkirchen +31707 Heeßen, Bad Eilsen +31749 Auetal +31711 Luhden +31710 Buchholz +32423 Minden +31604 Raddestorf +31553 Auhagen, Sachsenhagen +31556 Wölpinghausen +31715 Meerbeck +31702 Lüdersfeld +31691 Helpsen, Seggebruch +31717 Nordsehl +31688 Nienstädt +31718 Pollhagen +31719 Wiedensahl +31547 Rehburg-Loccum +31712 Niederwöhren +31655 Stadthagen +31708 Ahnsen +31693 Hespe +31714 Lauenhagen +37691 Boffzen, Derental +37603 Holzminden +37697 Lauenförde +37699 Fürstenberg +31515 Wunstorf +31860 Emmerthal +31020 Salzhemmendorf +31863 Coppenbrügge +31174 Schellerten +31167 Bockenem +31311 Uetze +37586 Dassel +37194 Bodenfelde, Wahlsburg +31535 Neustadt am Rübenberge +30890 Barsinghausen +30974 Wennigsen (Deister) +31832 Springe +30982 Pattensen +30855 Langenhagen +37632 Holzen, Eschershausen, Eimen +38723 Seesen +37574 Einbeck, Kreiensen +30916 Isernhagen +37627 Stadtoldendorf u.a. +31868 Ottenstein +37619 Bodenwerder, Pegestorf, Kirchbrak, Hehlen +37647 Brevörde, Polle, Vahlbruch +37649 Heinsen +31089 Duingen +37620 Halle +37635 Lüerdissen +37640 Golmbach +37633 Dielmissen +37639 Bevern +37643 Negenborn +37642 Holenberg +37186 Moringen +37589 Kalefeld +37154 Northeim +37191 Katlenburg-Lindau +31079 Sibbesse +31028 Gronau (Leine) +31073 Grünenplan, Delligsen +31061 Alfeld (Leine) +31084 Freden (Leine) +31162 Bad Salzdetfurth +37581 Bad Gandersheim +31195 Lamspringe +31867 Pohle, Lauenau, Messenkamp, Hülsede u.a. +31848 Bad Münder am Deister +31789 Hameln +31787 Hameln +31785 Hameln +31008 Elze +31036 Eime +31542 Bad Nenndorf +31558 Hagenburg +31698 Lindhorst +31699 Beckedorf +31700 Heuerßen +31552 Apelern, Rodenberg +31559 Haste, Hohnhorst +31555 Suthfeld +30823 Garbsen +30827 Garbsen +30826 Garbsen +30926 Seelze +30952 Ronnenberg +30989 Gehrden +30455 Hannover +30453 Hannover +30419 Hannover +31171 Nordstemmen +31157 Sarstedt +31180 Giesen +31139 Hildesheim +31249 Hohenhameln +31135 Hildesheim +31141 Hildesheim +31177 Harsum +31137 Hildesheim +31199 Diekholzen +31188 Holle +31191 Algermissen +31134 Hildesheim +30165 Hannover +30966 Hemmingen +30880 Laatzen +30459 Hannover +30853 Langenhagen +30175 Hannover +30655 Hannover +30173 Hannover +30659 Hannover +30629 Hannover +30519 Hannover +30539 Hannover +30559 Hannover +30457 Hannover +30521 Hannover +30451 Hannover +30167 Hannover +30449 Hannover +30169 Hannover +30159 Hannover +30163 Hannover +30161 Hannover +30171 Hannover +30851 Langenhagen +30179 Hannover +30177 Hannover +30625 Hannover +30627 Hannover +30657 Hannover +31319 Sehnde +31275 Lehrte +31303 Burgdorf +49453 Barver, Dickel, Hemsloh, Rehden, Wetschen, Wehrblecker Heide +49406 Barnstorf, Eydelstedt, Drentwede +49419 Wagenfeld +31632 Husum +31608 Marklohe +27404 Zeven, Elsdorf +27367 Sottrum, Reeßum, Bötersen u.a. +27412 Tarmstedt, Breddorf u.a. +27628 Hagen +27777 Ganderkesee +27711 Osterholz-Scharmbeck +28865 Lilienthal +28870 Ottersberg +31600 Uchte +27245 Bahrenborstel, Barenburg, Kirchdorf +27259 Freistatt, Varrel, Wehrbleck +27232 Sulingen +27243 Harpstedt, Groß Ippener, Colnrade u.a. +27239 Twistringen +27251 Neuenkirchen, Scholen +27252 Schwaförden +27211 Bassum +27257 Affinghausen und Sudwalde +27248 Ehrenburg +28857 Syke +31621 Pennigsehl +31595 Steyerberg +31592 Stolzenau +27249 Maasen, Mellinghausen +27246 Borstel +31618 Liebenau +31629 Estorf +31628 Landesbergen +31633 Leese +31582 Nienburg/Weser +31619 Binnen +31613 Wietzen +27254 Siedenburg, Staffhorst +27327 Martfeld, Schwarme +27305 Bruchhausen-Vilsen, Süstedt +27330 Asendorf +27324 Eystrup, Hassel u.a. +31627 Rohrsen +27333 Schweringen, Warpe, Bücken +31626 Haßbergen +31609 Balge +27318 Hoya, Hoyerhagen, Hilgermissen +27313 Dörverden +31623 Drakenburg +27755 Delmenhorst +27753 Delmenhorst +27751 Delmenhorst +28816 Stuhr +28844 Weyhe +27749 Delmenhorst +28790 Schwanewede +27809 Lemwerder +27729 Hambergen, Holste u.a. +27721 Ritterhude +27321 Thedinghausen, Emtinghausen +27339 Riede +28832 Achim +28876 Oyten +28325 Bremen +27283 Verden +27299 Langwedel +27337 Blender +27726 Worpswede +28879 Grasberg +31638 Stöckse +31637 Rodewald +31636 Linsburg +31634 Steimbke +29643 Neuenkirchen +21385 Oldendorf (Luhe), Amelinghausen, Rehlingen +21244 Buchholz in der Nordheide +21255 Tostedt, Kakenstorf u.a. +29303 Bergen, Lohheide u.a. +29693 Hodenhagen u.a. +29664 Walsrode, Ostenholz +29227 Celle +29229 Celle, Wittbeck +29223 Celle +27386 Bothel, Kirchwalsede u.a. +29308 Winsen (Aller) +29649 Wietzendorf +27383 Scheeßel +29633 Munster +21258 Heidenau +29640 Schneverdingen, Heimbuch +29646 Bispingen +29683 Bad Fallingbostel, Osterheide +27356 Rotenburg +29320 Hermannsburg +29328 Faßberg +30938 Burgwedel +29690 Schwarmstedt u.a. +30900 Wedemark +31622 Heemsen +27336 Rethem (Aller), Häuslingen, Frankenfeld +27308 Kirchlinteln +29323 Wietze +30669 Langenhagen (Flughafen) +29225 Celle +29313 Hambühren +29336 Nienhagen +29352 Adelheidsdorf +29221 Celle +27374 Visselhövede +29699 Bomlitz +27419 Sittensen u.a. +27389 Fintel, Lauenbrück u.a. +29614 Soltau +21259 Otter +21256 Handeloh +21274 Undeloh +21261 Welle +21272 Eggestorf +21376 Salzhausen +21271 Hanstedt, Asendorf +21442 Toppenstedt +21438 Brackel +21439 Marxen +21266 Jesteburg +37445 Walkenried +37412 Herzberg, Elbingerode, Hörden +37434 Gieboldehausen, Rhumequelle +37115 Duderstadt +37431 Bad Lauterberg +37441 Bad Sachsa +37539 Bad Grund +37197 Hattorf +37199 Wulften +37520 Osterode am Harz +37444 Braunlage +38170 Schöppenstedt +38678 Clausthal-Zellerfeld, Oberschulenberg +38667 Bad Harzburg, Torfhaus +38690 Goslar +38312 Dorstadt, Flöthe, Börßum u.a. +38315 Schladen-Werla +38536 Meinersen +38551 Ribbesbüttel +38709 Wildemann +38707 Altenau, Schulenberg +38729 Hahausen, Lutter, Wallmoden +38685 Langelsheim +38279 Sehlde +38644 Goslar +38642 Goslar +38259 Salzgitter +38704 Liebenburg +38640 Goslar +38700 Braunlage +31185 Söhlde +38228 Salzgitter +38272 Burgdorf +38271 Baddeckenstedt +38274 Elbe +38275 Haverlah +38277 Heere +31246 Lahstedt +38268 Lengede +38304 Wolfenbüttel +38122 Braunschweig +38120 Braunschweig +38229 Salzgitter +38239 Salzgitter +38159 Vechelde +38226 Salzgitter +31234 Edemissen +31224 Peine +31228 Peine +31241 Ilsede +31226 Peine +38110 Braunschweig +38116 Braunschweig +38114 Braunschweig +38112 Braunschweig +38176 Wendeburg +38179 Schwülper +38528 Adenbüttel +38530 Didderse +38533 Vordorf +38542 Leiferde +38543 Hillerse +38531 Rötgesbüttel +38118 Braunschweig +38302 Wolfenbüttel +38300 Wolfenbüttel +38126 Braunschweig +38173 Sickte, Dettum u.a. +38321 Denkte +38324 Kissenbrück +38319 Remlingen +38322 Hedeper +38327 Semmenstedt +38162 Cremlingen +38329 Wittmar +38124 Braunschweig +38373 Süpplingen, Frellstedt +38154 Königslutter am Elm +38375 Räbke +38325 Roklum +38381 Jerxheim +38382 Beierstedt +38384 Gevensleben +38387 Söllingen +38364 Schöningen +38376 Süpplingenburg +38378 Warberg +38100 Braunschweig +38102 Braunschweig +38106 Braunschweig +38104 Braunschweig +38108 Braunschweig +38442 Wolfsburg +38527 Meine +38165 Lehre +38547 Calberlah +38550 Isenbüttel +38553 Wasbüttel +38554 Weyhausen +38368 Rennau, Querenhorst, Mariental, Grasleben +38440 Wolfsburg +38444 Wolfsburg +38446 Wolfsburg +38448 Wolfsburg +38458 Velpke +38461 Danndorf +38462 Grafhorst +38464 Groß Twülpstedt +38350 Helmstedt +38372 Büddenstedt +38379 Wolsdorf +38459 Bahrdorf +29331 Lachendorf +29339 Wathlingen +29342 Wienhausen +29353 Ahnsbeck +29356 Bröckel +29358 Eicklingen +29364 Langlingen +29556 Suderburg +21354 Bleckede +19273 Amt Neuhaus, Stapel +29378 Wittingen +29386 Hankensbüttel, Obernholz, Dedelstorf +29348 Eschede +29473 Göhrde +29559 Wrestedt +21445 Wulfsen +29565 Wriedel +29355 Beedenbostel +29365 Sprakensehl +29456 Hitzacker (Elbe) +29562 Suhlendorf +29587 Natendorf +29499 Zernien +29525 Uelzen +29351 Eldingen +38518 Gifhorn +38559 Wagenhoff, Ringelah +29362 Hohne +29369 Ummern +38539 Müden (Aller) +29393 Groß Oesingen +29578 Eimke +29345 Unterlüß +29359 Habighorst +29361 Höfer +29367 Steinhorst +29379 Wittingen +29392 Wesendorf +29396 Schönewörde +29399 Wahrenholz +38524 Sassenburg +38556 Bokensdorf +38557 Osloß +38479 Tappenbeck +38470 Parsau +38465 Brome +38467 Bergfeld +38468 Ehra-Lessien +38477 Jembke +38476 Barwedel +38471 Rühen +38473 Tiddische +38474 Tülau +29389 Bad Bodenteich +29394 Lüder +29465 Schnega +29468 Bergen +29594 Soltendiek +29582 Hanstedt +29574 Ebstorf +29581 Gerdau +29593 Schwienau +29576 Barum +21394 Kirchgellersen, Westergellersen, Südergellersen +21391 Reppenstedt, Lüneburg +21358 Mechtersen +21441 Garstedt +21444 Vierhöfen +21386 Betzendorf +21388 Soderstorf +21406 Melbeck, Barnstedt +21335 Lüneburg +21379 Scharnebeck, Echem, Lüdersburg, Rullstorf +21339 Lüneburg +21337 Lüneburg +21357 Bardowick, Wittorf, Barum +21360 Vögelsen +21365 Adendorf +21407 Deutsch Evern +29553 Bienenbüttel +21409 Embsen +21403 Wendisch Evern +29549 Bad Bevensen +29579 Emmendorf +29584 Himbergen +29588 Oetzen +29590 Rätzlingen +29591 Römstedt +29599 Weste +29459 Clenze +29496 Waddeweitz +29571 Rosche +29597 Stoetze +29585 Jelmstorf +21368 Dahlenburg +21397 Barendorf, Vastorf +21398 Neetze +21400 Reinstorf +21401 Thomasburg +29575 Altenmedingen +21369 Nahrendorf +21371 Tosterglope +29490 Neu Darchau +29491 Prezelle +29471 Gartow +29476 Gusborn +29481 Karwitz +29439 Lüchow +29451 Dannenberg +29462 Wustrow +29482 Küsten +29487 Luckau (Wendland) +29479 Jameln +29475 Gorleben +29484 Langendorf +29485 Lemgow +29488 Lübbow +29494 Trebel +29497 Woltersdorf +29472 Damnatz +29478 Höhbeck +29493 Schnackenburg +26427 Esens, Neuharlingersiel u.a. +26629 Großefehn +26723 Emden +26736 Krummhörn +26571 Juist, Memmert +26506 Norden +26721 Emden +26725 Emden +26529 Upgant-Schott, Osteel u.a. +26624 Südbrookmerland +26759 Hinte +26632 Ihlow +26607 Aurich +26605 Aurich +26603 Aurich +26524 Hage, Halbemond u.a. +26532 Großheide +26548 Norderney +26553 Dornum +26487 Blomberg, Neuschoo +26556 Westerholt, Schweindorf u.a. +26489 Ochtersum +26579 Baltrum +26465 Langeoog +26639 Wiesmoor +26954 Nordenham +26969 Butjadingen +26409 Wittmund +26452 Sande +26345 Bockhorn +26349 Jade +26446 Friedeburg +26340 Zetel +26419 Schortens +26434 Wangerland +26441 Jever +26316 Varel +26937 Stadland +26936 Stadland +26384 Wilhelmshaven +26382 Wilhelmshaven +26389 Wilhelmshaven +26386 Wilhelmshaven +26388 Wilhelmshaven +26474 Spiekeroog +26486 Wangerooge +26935 Stadland +26919 Brake +27616 Beverstedt +27624 Geestland +27446 Selsingen +27432 Bremervörde +27478 Cuxhaven +21709 Himmelpforten +27612 Loxstedt +27449 Kutenholz +21730 Balje +21706 Drochtersen +21712 Großenwörden +27619 Schiffdorf +27607 Geestland +27639 Wurster Nordseeküste +21776 Wanna +27442 Gnarrenburg +21775 Ihlienworth +21769 Lamstedt +21770 Mittelstenahe +21772 Stinstedt +21782 Bülkau +21789 Wingst +21726 Oldendorf +21727 Estorf +21745 Hemmoor +21755 Hechthausen +21756 Osten +27476 Cuxhaven +27472 Cuxhaven +21765 Nordleda +27474 Cuxhaven +21763 Neuenkirchen +21762 Otterndorf +21785 Neuhaus (Oste) +21781 Cadenberge +21732 Krummendeich +21734 Oederquart +21787 Oberndorf +21698 Harsefeld +21702 Ahlerstedt +21717 Fredenbeck +21435 Stelle +25377 Kollmar, Pagensand +21640 Horneburg +21614 Buxtehude +21635 Jork +21641 Apensen +21643 Beckdorf +21644 Sauensiek +21646 Halvesbostel +21649 Regesbostel +21684 Stade +21682 Stade +21683 Stade +21710 Engelschoff +21714 Hammah +21720 Grünendeich +21723 Hollern-Twielenfleth +21680 Stade +21739 Dollern +21129 Hamburg +21629 Neu Wulmstorf +21279 Hollenstedt, Drestedt u.a. +21224 Rosengarten +21647 Moisburg +21218 Seevetal +21228 Harmstorf +21227 Bendestorf +21220 Seevetal +21217 Seevetal +21729 Freiburg (Elbe) +21737 Wischhafen +21423 Winsen +21449 Radbruch +19258 Boizenburg, Gresse, Greven u.a. +21382 Brietlingen +21380 Artlenburg +21395 Tespe +21436 Marschacht +21447 Handorf +21522 Hohnstorf (Elbe), Hittbergen +53881 Euskirchen +52080 Aachen +52076 Aachen +53945 Blankenheim +53949 Dahlem +53940 Hellenthal +53925 Kall +53894 Mechernich +53902 Bad Münstereifel +53937 Schleiden +52388 Nörvenich +52393 Hürtgenwald +52156 Monschau +52152 Simmerath +52066 Aachen +52074 Aachen +52064 Aachen +52068 Aachen +52222 Stolberg (Rhld.) +52078 Aachen +52224 Stolberg +52159 Roetgen +52223 Stolberg (Rhld.) +53947 Nettersheim +52355 Düren +52351 Düren +52379 Langerwehe +52385 Nideggen +52372 Kreuzau +52396 Heimbach +53909 Zülpich +50374 Erftstadt +52391 Vettweiß +52538 Gangelt, Selfkant +52070 Aachen +52072 Aachen +52531 Übach-Palenberg +52134 Herzogenrath +50129 Bergheim +47447 Moers +47652 Weeze +47506 Neukirchen-Vluyn +47669 Wachtendonk +41812 Erkelenz +41372 Niederkrüchten +41379 Brüggen +52445 Titz +52428 Jülich +52441 Linnich +52511 Geilenkirchen +46519 Alpen +46569 Hünxe +52062 Aachen +52457 Aldenhoven +52499 Baesweiler +52146 Würselen +52477 Alsdorf +52249 Eschweiler +52525 Waldfeucht, Heinsberg +41849 Wassenberg +41179 Mönchengladbach +41836 Hückelhoven +41844 Wegberg +41366 Schwalmtal +52349 Düren +52353 Düren +50181 Bedburg +50189 Elsdorf +52382 Niederzier +52459 Inden +50171 Kerpen +50170 Kerpen +50169 Kerpen +50127 Bergheim +50126 Bergheim +52399 Merzenich +41061 Mönchengladbach +41068 Mönchengladbach +41169 Mönchengladbach +41189 Mönchengladbach +41199 Mönchengladbach +41238 Mönchengladbach +41069 Mönchengladbach +41352 Korschenbroich +41363 Jüchen +41065 Mönchengladbach +41236 Mönchengladbach +41239 Mönchengladbach +41460 Neuss +41464 Neuss +41515 Grevenbroich +41516 Grevenbroich +41517 Grevenbroich +41542 Dormagen +41468 Neuss +41469 Neuss +41470 Neuss +41472 Neuss +41564 Kaarst +41569 Rommerskirchen +41466 Neuss +41751 Viersen +47638 Straelen +41334 Nettetal +47627 Kevelaer-Kervenheim +47625 Kevelaer-Wetten +47626 Kevelaer-Winnekendonk +47623 Kevelaer-Mitte +47624 Kevelaer-Twisteden +47608 Geldern +41748 Viersen +41749 Viersen +41063 Mönchengladbach +41066 Mönchengladbach +47804 Krefeld +47839 Krefeld +47929 Grefrath +47906 Kempen +47918 Tönisvorst +47877 Willich +41747 Viersen +41462 Neuss +47800 Krefeld +47802 Krefeld +47803 Krefeld +47807 Krefeld +47809 Krefeld +47829 Krefeld +40667 Meerbusch +40668 Meerbusch +40670 Meerbusch +47226 Duisburg +47239 Duisburg +47259 Duisburg +40474 Düsseldorf +40549 Düsseldorf +47798 Krefeld +47805 Krefeld +47799 Krefeld +47229 Duisburg +47665 Sonsbeck +47661 Issum +47475 Kamp-Lintfort +47509 Rheurdt +47647 Kerken +46535 Dinslaken +47198 Duisburg +47199 Duisburg +47441 Moers +47443 Moers +47445 Moers +47179 Duisburg +47139 Duisburg +47495 Rheinberg +46562 Voerde (Niederrhein) +47228 Duisburg +47178 Duisburg +47559 Kranenburg +47533 Kleve +46399 Bocholt +46395 Bocholt +46487 Wesel +47574 Goch +47546 Kalkar +46446 Emmerich am Rhein +46514 Schermbeck +46499 Hamminkeln +46414 Rhede +48691 Vreden +47551 Bedburg-Hau +47589 Uedem +46459 Rees +46419 Isselburg +46509 Xanten +46485 Wesel +46483 Wesel +46397 Bocholt +53332 Bornheim +53359 Rheinbach +53879 Euskirchen +53919 Weilerswist +53913 Swisttal +53177 Bonn +53125 Bonn +53175 Bonn +53227 Bonn +53229 Bonn +53225 Bonn +53117 Bonn +53127 Bonn +53123 Bonn +53844 Troisdorf +53859 Niederkassel +53347 Alfter +53340 Meckenheim +53343 Wachtberg +53757 Sankt Augustin +53115 Bonn +53129 Bonn +53113 Bonn +53119 Bonn +53121 Bonn +53111 Bonn +53604 Bad Honnef +53639 Königswinter +53773 Hennef (Sieg) +53179 Bonn +53173 Bonn +53783 Eitorf +51570 Windeck +57290 Neunkirchen +57299 Burbach +50935 Köln +50829 Köln +50737 Köln +50859 Köln +50739 Köln +50933 Köln +50997 Köln +50259 Pulheim +50226 Frechen +50354 Hürth +50321 Brühl +51688 Wipperfürth +51766 Engelskirchen +51789 Lindlar +58553 Halver +58566 Kierspe +50858 Köln +50931 Köln +50969 Köln +50937 Köln +50939 Köln +50823 Köln +50825 Köln +50827 Köln +53842 Troisdorf +50735 Köln +50996 Köln +50999 Köln +51105 Köln +51109 Köln +51061 Köln +51149 Köln +51147 Köln +51069 Köln +51067 Köln +51107 Köln +51143 Köln +51429 Bergisch Gladbach +51469 Bergisch Gladbach +51465 Bergisch Gladbach +51427 Bergisch Gladbach +50389 Wesseling +51145 Köln +50674 Köln +50678 Köln +50677 Köln +50679 Köln +50968 Köln +50733 Köln +50668 Köln +50676 Köln +50670 Köln +50672 Köln +50667 Köln +51063 Köln +51103 Köln +51065 Köln +40764 Langenfeld +50769 Köln +50765 Köln +51371 Leverkusen +41539 Dormagen +41540 Dormagen +41541 Dormagen +40595 Düsseldorf +40589 Düsseldorf +40721 Hilden, Düsseldorf +40724 Hilden +40593 Düsseldorf +40599 Düsseldorf +40223 Düsseldorf +40225 Düsseldorf +40221 Düsseldorf +40789 Monheim am Rhein +50767 Köln +40591 Düsseldorf +40597 Düsseldorf +42697 Solingen +42699 Solingen +42657 Solingen +42719 Solingen +42653 Solingen +51377 Leverkusen +51373 Leverkusen +51381 Leverkusen +51467 Bergisch Gladbach +42651 Solingen +42659 Solingen +42349 Wuppertal +42781 Haan +51519 Odenthal +51399 Burscheid +42799 Leichlingen +51379 Leverkusen +51375 Leverkusen +40723 Hilden +42655 Solingen +53840 Troisdorf +53809 Ruppichteroth +53804 Much +53819 Neunkirchen-Seelscheid +53797 Lohmar +53721 Siegburg +51503 Rösrath +51491 Overath +51645 Gummersbach +51545 Waldbröl +51588 Nümbrecht +51674 Wiehl +42855 Remscheid +42857 Remscheid +42859 Remscheid +42897 Remscheid +42899 Remscheid +42477 Radevormwald +42499 Hückeswagen +51515 Kürten +42929 Wermelskirchen +42853 Remscheid +51647 Gummersbach +51709 Marienheide +51643 Gummersbach +40545 Düsseldorf +40547 Düsseldorf +40219 Düsseldorf +40489 Düsseldorf +40629 Düsseldorf +40627 Düsseldorf +40625 Düsseldorf +40880 Ratingen +40882 Ratingen +40885 Ratingen +47269 Duisburg +45219 Essen +47055 Duisburg +45479 Mülheim an der Ruhr +45481 Mülheim an der Ruhr +45470 Mülheim an der Ruhr +40699 Erkrath +42579 Heiligenhaus +40472 Düsseldorf +40468 Düsseldorf +40477 Düsseldorf +40229 Düsseldorf +40231 Düsseldorf +40479 Düsseldorf +40211 Düsseldorf +40210 Düsseldorf +40212 Düsseldorf +40213 Düsseldorf +40215 Düsseldorf +40227 Düsseldorf +40233 Düsseldorf +40235 Düsseldorf +40237 Düsseldorf +40239 Düsseldorf +40217 Düsseldorf +40470 Düsseldorf +40476 Düsseldorf +40878 Ratingen +47249 Duisburg +47279 Duisburg +40883 Ratingen +42117 Wuppertal +42111 Wuppertal +42113 Wuppertal +42327 Wuppertal +42551 Velbert +42555 Velbert +42553 Velbert +45529 Hattingen +45257 Essen +45259 Essen +45239 Essen +45133 Essen +45149 Essen +40822 Mettmann +42489 Wülfrath +42329 Wuppertal +42115 Wuppertal +42105 Wuppertal +42109 Wuppertal +42549 Velbert +47053 Duisburg +47051 Duisburg +47059 Duisburg +46244 Bottrop +46539 Dinslaken +46049 Oberhausen +46119 Oberhausen +46117 Oberhausen +46147 Oberhausen +46149 Oberhausen +46145 Oberhausen +47169 Duisburg +47167 Duisburg +47119 Duisburg +47166 Duisburg +47137 Duisburg +47138 Duisburg +45478 Mülheim an der Ruhr +45473 Mülheim an der Ruhr +45359 Essen +46242 Bottrop +46240 Bottrop +46236 Bottrop +47057 Duisburg +47058 Duisburg +45476 Mülheim an der Ruhr +45475 Mülheim an der Ruhr +45472 Mülheim an der Ruhr +45468 Mülheim an der Ruhr +46047 Oberhausen +46045 Oberhausen +45357 Essen +46537 Dinslaken +45147 Essen +45145 Essen +45881 Gelsenkirchen +45896 Gelsenkirchen +45892 Gelsenkirchen +45699 Herten +45701 Herten +45894 Gelsenkirchen +45964 Gladbeck +45966 Gladbeck +45883 Gelsenkirchen +44653 Herne +44649 Herne +45897 Gelsenkirchen +44879 Bochum +45134 Essen +45138 Essen +45144 Essen +45139 Essen +45327 Essen +45279 Essen +45276 Essen +45329 Essen +45356 Essen +45141 Essen +44867 Bochum +44869 Bochum +45307 Essen +45886 Gelsenkirchen +45891 Gelsenkirchen +45136 Essen +45131 Essen +45130 Essen +45128 Essen +45355 Essen +45143 Essen +45127 Essen +45326 Essen +45277 Essen +45289 Essen +45309 Essen +45884 Gelsenkirchen Rotthausen +45879 Gelsenkirchen +44866 Bochum +46238 Bottrop +45968 Gladbeck +45899 Gelsenkirchen +45889 Gelsenkirchen +45888 Gelsenkirchen +42119 Wuppertal +42279 Wuppertal +42281 Wuppertal +42287 Wuppertal +42369 Wuppertal +42389 Wuppertal +42399 Wuppertal +45525 Hattingen +45527 Hattingen +58456 Witten +58256 Ennepetal +58332 Schwelm +45549 Sprockhövel +58300 Wetter (Ruhr) +58285 Gevelsberg +42103 Wuppertal +42107 Wuppertal +42283 Wuppertal +42285 Wuppertal +42275 Wuppertal +42277 Wuppertal +42289 Wuppertal +58119 Hagen +58093 Hagen +58089 Hagen +58135 Hagen +58099 Hagen +58091 Hagen +58339 Breckerfeld +58579 Schalksmühle +58239 Schwerte +58313 Herdecke +58097 Hagen +58095 Hagen +44359 Dortmund +44388 Dortmund +58453 Witten +45665 Recklinghausen +45659 Recklinghausen +45661 Recklinghausen +44579 Castrop-Rauxel +44577 Castrop-Rauxel +44581 Castrop-Rauxel +44894 Bochum +44628 Herne +44625 Herne +58455 Witten +44809 Bochum +44791 Bochum +44795 Bochum +44797 Bochum +44799 Bochum +44801 Bochum +44803 Bochum +45731 Waltrop +44793 Bochum +44807 Bochum +44787 Bochum +44789 Bochum +58452 Witten +44805 Bochum +44892 Bochum +44651 Herne +44652 Herne +44623 Herne +44629 Herne +45657 Recklinghausen +45663 Recklinghausen +44575 Castrop-Rauxel +44627 Herne +44369 Dortmund +44149 Dortmund +44227 Dortmund +44141 Dortmund +44339 Dortmund +44265 Dortmund +44287 Dortmund +44143 Dortmund +44379 Dortmund +44147 Dortmund +44309 Dortmund +44329 Dortmund +44269 Dortmund +44267 Dortmund +44229 Dortmund +44139 Dortmund +44532 Lünen +44536 Lünen +58454 Witten +44225 Dortmund +44137 Dortmund +44263 Dortmund +44357 Dortmund +44145 Dortmund +44135 Dortmund +44328 Dortmund +51580 Reichshof +51597 Morsbach +59757 Arnsberg +58708 Menden +57250 Netphen +57271 Hilchenbach +57319 Bad Berleburg +57368 Lennestadt +57399 Kirchhundem +57413 Finnentrop +57482 Wenden +57392 Schmallenberg +59872 Meschede +59889 Eslohe +58809 Neuenrade +58840 Plettenberg +59846 Sundern +59909 Bestwig +58540 Meinerzhagen +59192 Bergkamen +57080 Siegen +57078 Siegen +57072 Siegen +57223 Kreuztal +57258 Freudenberg +58515 Lüdenscheid +57489 Drolshagen +51702 Bergneustadt +58849 Herscheid +57439 Attendorn +57462 Olpe +57074 Siegen +57076 Siegen +57234 Wilnsdorf +57334 Bad Laasphe +57339 Erndtebrück +58509 Lüdenscheid +58513 Lüdenscheid +58640 Iserlohn +58644 Iserlohn +58642 Iserlohn +58636 Iserlohn +58675 Hemer +58791 Werdohl +58762 Altena +58769 Nachrodt-Wiblingwerde +58507 Lüdenscheid +58511 Lüdenscheid +58638 Iserlohn +58710 Menden +58802 Balve +44319 Dortmund +59199 Bönen +59427 Unna +59425 Unna +58730 Fröndenberg/Ruhr +59439 Holzwickede +59174 Kamen +44289 Dortmund +59423 Unna +58706 Menden +59069 Hamm +59759 Arnsberg +59755 Arnsberg +58739 Wickede (Ruhr) +59469 Ense +59457 Werl +59514 Welver +59823 Arnsberg +59821 Arnsberg +59581 Warstein +59519 Möhnesee +59494 Soest +59505 Bad Sassendorf +59602 Rüthen +59597 Erwitte +59609 Anröchte +33154 Salzkotten +33142 Büren +33165 Lichtenau +59964 Medebach +59955 Winterberg +34414 Warburg +34439 Willebadessen +59969 Bromskirchen, Hallenberg +59929 Brilon +59939 Olsberg +34431 Marsberg +59590 Geseke +33181 Wünnenberg +34434 Borgentreich +46284 Dorsten +46282 Dorsten +46348 Raesfeld +46286 Dorsten +48485 Neuenkirchen +48496 Hopsten +48268 Greven +48477 Hörstel +46325 Borken +46359 Heiden +45721 Haltern am See +59348 Lüdinghausen +59368 Werne +59387 Ascheberg +48249 Dülmen +48727 Billerbeck +48720 Rosendahl +48341 Altenberge +48624 Schöppingen +48739 Legden +48683 Ahaus +48703 Stadtlohn +48607 Ochtrup +48493 Wettringen +45772 Marl +45770 Marl +45768 Marl +48734 Reken +46354 Südlohn +46342 Velen +48712 Gescher +48653 Coesfeld +45711 Datteln +45739 Oer-Erkenschwick +59399 Olfen +44534 Lünen +59379 Selm +59394 Nordkirchen +48308 Senden +48301 Nottuln +48163 Münster +48161 Münster +48329 Havixbeck +48619 Heek +48599 Gronau +48565 Steinfurt +48366 Laer +48612 Horstmar +48629 Metelen +48432 Rheine +48282 Emsdetten +48356 Nordwalde +48431 Rheine +48429 Rheine +59075 Hamm +59077 Hamm +59067 Hamm +48317 Drensteinfurt +33790 Halle (Westfalen) +49479 Ibbenbüren +32351 Stemwede +49497 Mettingen +49504 Lotte +49525 Lengerich +59269 Beckum +59320 Ennigerloh +59510 Lippetal +48231 Warendorf +48291 Telgte +48346 Ostbevern +48351 Everswinkel +48324 Sendenhorst +59227 Ahlen +59229 Ahlen +59063 Hamm +59071 Hamm +59073 Hamm +59065 Hamm +48165 Münster +48159 Münster +48167 Münster +48157 Münster +48155 Münster +48147 Münster +48153 Münster +48151 Münster +48149 Münster +48143 Münster +48145 Münster +59302 Oelde +59329 Wadersloh +33449 Langenberg +33397 Rietberg +33378 Rheda-Wiedenbrück +59558 Lippstadt +59555 Lippstadt +59556 Lippstadt +59557 Lippstadt +33442 Herzebrock-Clarholz +33775 Versmold +33428 Harsewinkel +48336 Sassenberg +48361 Beelen +33649 Bielefeld +33332 Gütersloh +33334 Gütersloh +33803 Steinhagen +33330 Gütersloh +49545 Tecklenburg +48369 Saerbeck +49549 Ladbergen +49536 Lienen +49509 Recke +49477 Ibbenbüren +49492 Westerkappeln +33824 Werther (Westf.) +33829 Borgholzhausen +33129 Delbrück +33189 Schlangen +33161 Hövelhof +33758 Schloß Holte-Stukenbrock +32369 Rahden +32339 Espelkamp +32469 Petershagen +32657 Lemgo +32689 Kalletal +32683 Barntrup +33106 Paderborn +33102 Paderborn +33178 Borchen +33098 Paderborn +33104 Paderborn +33100 Paderborn +33175 Bad Lippspringe +33647 Bielefeld +33699 Bielefeld +33415 Verl +33615 Bielefeld +33659 Bielefeld +33689 Bielefeld +33335 Gütersloh +33619 Bielefeld +33617 Bielefeld +33609 Bielefeld +33719 Bielefeld +33607 Bielefeld +33333 Bertelsmann +33604 Bielefeld +33602 Bielefeld +33605 Bielefeld +33818 Leopoldshöhe +33813 Oerlinghausen +32108 Bad Salzuflen +32107 Bad Salzuflen +32758 Detmold +32760 Detmold +32791 Lage +32832 Augustdorf +33014 Bad Driburg +33184 Altenbeken +33039 Nieheim +32805 Horn-Bad Meinberg +32839 Steinheim +33034 Brakel +37696 Marienmünster +32756 Detmold +32825 Blomberg +32694 Dörentrup +31812 Bad Pyrmont +32676 Lügde +32816 Schieder-Schwalenberg +33739 Bielefeld +33729 Bielefeld +32051 Herford +32120 Hiddenhausen +32130 Enger +32139 Spenge +32257 Bünde +32278 Kirchlengern +32289 Rödinghausen +32609 Hüllhorst +33613 Bielefeld +33611 Bielefeld +32052 Herford +32049 Herford +32105 Bad Salzuflen +32547 Bad Oeynhausen +32549 Bad Oeynhausen +32545 Bad Oeynhausen +32584 Löhne +32602 Vlotho +32312 Lübbecke +32361 Preußisch Oldendorf +32429 Minden +32479 Hille +32457 Porta Westfalica +32699 Extertal +32423 Minden +32427 Minden +32425 Minden +37671 Höxter +37688 Beverungen +54294 Trier +54331 Pellingen +54518 Niersbach, Sehlem, Plein u.a. +54298 Welschbillig, Igel, Aach +54636 Rittersdorf u.a. +54675 Körperich u.a. +54439 Saarburg +54457 Wincheringen +54308 Langsur +54441 Ayl, Trassem u.a. +54453 Nittel +54456 Tawern +54316 Pluwig +54329 Konz +54451 Irsch +54450 Freudenburg +54455 Serrig +54314 Zerf +54429 Schillingen +54296 Trier +54317 Osburg, Gusterath, Farschweiler, Kasel u.a. +54459 Wiltingen +54332 Wasserliesch +54634 Bitburg +54310 Ralingen +54669 Bollendorf +54668 Ferschweiler +54646 Bettingen +54666 Irrel +54309 Newel +54292 Trier +54311 Trierweiler +54344 Kenn +54306 Kordel +54293 Trier +54338 Schweich +54313 Zemmer +54318 Mertesdorf +54664 Preist +54662 Speicher +54290 Trier +54295 Trier +66957 Vinningen, Trulben, Ruppertsweiler u.a. +76744 Wörth +66996 Fischbach, Erfweiler u.a. +76891 Bruchweiler-Bärenbach u.a. +76887 Bad Bergzabern u.a. +76889 Klingenmünster u.a. +76872 Steinweiler +76768 Berg +76870 Kandel +76779 Scheibenhardt +76777 Neupotz +76767 Hagenbach +76776 Neuburg am Rhein +76764 Rheinzabern +76751 Jockgrim +55758 Niederwörresbach +54426 Malborn +66482 Zweibrücken +66500 Hornbach +66989 Höheinöd, Petersberg u.a. +66509 Rieschweiler-Mühlbach +66497 Contwig +66506 Maßweiler +66919 Obernheim-Kirchenarnbach u.a. +66501 Kleinbundenbach +66954 Pirmasens +66484 Battweiler u.a. +66504 Bottenbach +66917 Wallhalben u.a. +66987 Thaleischweiler-Fröschen +66503 Dellfeld +66507 Reifenberg +66909 Herschweiler-Pettersheim +66903 Altenkirchen +66871 Pfeffelbach +66914 Waldmohr +66904 Brücken (Pfalz) +66916 Breitenbach +66901 Schönenberg-Kübelberg +66869 Kusel +66887 Rammelsbach u.a. +66882 Hütschenhausen +66892 Bruchmühlbach-Miesau +66877 Ramstein-Miesenbach +66894 Bechhofen +66907 Glan-Münchweiler +66879 Steinwenden u.a. +66851 Mittelbrunn, Queidersbach u.a. +66849 Landstuhl +54413 Gusenburg +54411 Deuselbach, Hermeskeil, Rorodt +54421 Reinsfeld +54427 Kell am See +55765 Birkenfeld u.a. +55767 Brücken, Oberbrombach u.a. +54422 Neuhütten +54340 Leiwen u.a. +54320 Waldrach +54346 Mehring +54341 Fell +54524 Klausen +54498 Piesport +54347 Neumagen-Dhron +54523 Hetzerath, Dierscheid, Heckenmünster +54528 Salmtal +54349 Trittenheim +54343 Föhren +56843 Irmenach +56841 Traben-Trarbach +54424 Thalfang +54470 Bernkastel-Kues u.a. +54484 Maring-Noviand +54487 Wintrich +54472 Monzelfeld, Hochscheid u.a. +54486 Mülheim (Mosel) +54497 Morbach +55777 Berschweiler bei Baumholder +55779 Heimbach +55768 Hoppstädten-Weiersbach +55743 Idar-Oberstein +55774 Baumholder +55776 Berglangenbach, Ruschberg u.a. +66885 Altenglan +67749 Offenbach-Hundheim +67746 Langweiler +67756 Hinzweiler +67754 Eßweiler +67744 Medard, Rathskirchen u.a. +67742 Lauterecken u.a. +55606 Kirn +55608 Bergen +67745 Grumbach +55621 Hundsbach +55491 Büchenbeuren +55481 Kirchberg u.a. +55483 Dickenschied u.a. +55487 Sohren +55624 Rhaunen +55756 Herrstein +54483 Kleinich +55627 Merxheim +55619 Hennweiler +55618 Simmertal +55629 Seesbach +55566 Sobernheim +55499 Riesweiler +55490 Gemünden +55471 Tiefenbach u.a. +55626 Bundenbach +76848 Wilgartswiesen +66953 Pirmasens +66976 Rodalben +66955 Pirmasens +66981 Münchweiler an der Rodalb +66969 Lemberg +66978 Clausen +66994 Dahn +66999 Hinterweidenthal +67716 Heltersberg +67714 Waldfischbach-Burgalben +67657 Kaiserslautern +67663 Kaiserslautern +67304 Eisenberg (Pfalz) +67307 Göllheim +67098 Bad Dürkheim +67487 Maikammer +67435 Neustadt an der Weinstraße +55234 Albig +55270 Ober-Olm +67808 Steinbach, Weitersweiler, Bennhausen, Mörsfeld, Würzweiler, Ruppertsecken u.a. +76857 Albersweiler, Silz u.a. +76846 Hauenstein +76855 Annweiler am Trifels +76835 Rhodt u.a. +67480 Edenkoben +66862 Kindsbach +67705 Trippstadt u.a. +67655 Kaiserslautern +67688 Rodenbach +67685 Weilerbach u.a. +67731 Otterbach, Otterberg +67661 Kaiserslautern +67659 Kaiserslautern +67734 Katzweiler +67697 Otterberg +67735 Mehlbach +67706 Krickenbach +67707 Schopp +67718 Schmalenberg +67715 Geiselberg +67686 Mackenbach +67681 Sembach +67678 Mehlingen +67693 Waldleiningen, Fischbach +67305 Ramsen +67471 Elmstein +67319 Wattenheim +67475 Weidenthal +67468 Frankenstein, Neidenfels, Frankeneck +67691 Hochspeyer +67677 Enkenbach-Alsenborn +67680 Neuhemsbach +76863 Herxheim +76829 Landau in der Pfalz +76877 Offenbach an der Queich +76879 Hochstadt +76833 Frankweiler +76865 Insheim +76831 Billigheim-Ingenheim, Birkweiler +67483 Edesheim +67489 Kirrweiler (Pfalz) +67482 Venningen +67377 Gommersheim +76771 Hördt +76761 Rülzheim +76770 Hatzenbühl +76726 Germersheim +76774 Leimersheim +76756 Bellheim +76773 Kuhardt +67360 Lingenfeld +67363 Lustadt +67366 Weingarten (Pfalz) +67354 Römerberg +67373 Dudenhofen +67346 Speyer +67376 Harthausen +67378 Zeiskam +67361 Freisbach +67368 Westheim (Pfalz) +67365 Schwegenheim +67466 Lambrecht (Pfalz) +67273 Weisenheim am Berg +67472 Esthal +67317 Altleiningen +67256 Weisenheim am Sand +67146 Deidesheim +67157 Wachenheim an der Weinstraße +67271 Kindenheim +67434 Neustadt an der Weinstraße +67433 Neustadt an der Weinstraße +67473 Lindenberg +67152 Ruppertsberg +67316 Carlsberg +67147 Forst an der Weinstraße +67169 Kallstadt +67149 Meckenheim +67158 Ellerstadt +67161 Gönnheim +67136 Fußgönheim +67071 Ludwigshafen am Rhein +67105 Schifferstadt +67459 Böhl-Iggelheim +67227 Frankenthal (Pfalz) +67125 Dannstadt-Schauernheim +67065 Ludwigshafen am Rhein +67112 Mutterstadt +67117 Limburgerhof +67245 Lambsheim +67126 Hochdorf-Assenheim +67067 Ludwigshafen am Rhein +67374 Hanhofen +67069 Ludwigshafen am Rhein +67063 Ludwigshafen am Rhein +67454 Haßloch +67150 Niederkirchen bei Deidesheim +67159 Friedelsheim +67127 Rödersheim-Gronau +67167 Erpolzheim +67251 Freinsheim +67133 Maxdorf +67134 Birkenheide +67699 Schneckenhausen +67737 Olsbrücken +67759 Nußbach +67827 Becherbach +67823 Obermoschel, Schiersfeld +67724 Gundersweiler, Gonbach u.a. +67752 Wolfstein +67806 Rockenhausen, Bisterschied u.a. +67829 Callbach +67753 Rothselberg u.a. +67748 Odenbach +55590 Meisenheim +55592 Rehborn +67822 Winterborn, Waldgrehweiler, Niedermoschel u.a. +67700 Niederkirchen +67757 Kreimbach-Kaulbach +67732 Hirschhorn +67701 Schallodenbach +67728 Münchweiler an der Alsenz +67819 Kriegsfeld +67729 Sippersfeld +67725 Börrstadt +67295 Bolanden +67814 Dannenfels +67722 Winnweiler +67292 Kirchheimbolanden +67811 Dielkirchen +67294 Bischheim u.a. +67821 Alsenz +67813 Schwarzengraben, St. Alban, Gerbach +67817 Imsbach +67727 Lohnsfeld +67824 Feilbingert +55595 Hargesheim +55452 Guldental +55596 Waldböckelheim +55496 Argenthal +55569 Monzingen +55585 Norheim u.a. +55571 Odernheim am Glan +55442 Stromberg +55568 Staudernheim +55444 Seibersbach +67826 Hallgarten +55459 Aspisheim, Grolsheim +55411 Bingen am Rhein +55437 Ockenheim +55599 Gau-Bickelheim +55457 Gensingen +55576 Sprendlingen +55597 Wöllstein +55545 Bad Kreuznach +55546 Hackenheim +55450 Langenlonsheim +55583 Bad Kreuznach +55559 Bretzenheim +55543 Bad Kreuznach +55593 Rüdesheim +67297 Marnheim +67308 Albisheim (Pfrimm) +67310 Hettenleidelheim +67269 Grünstadt +67283 Obrigheim (Pfalz) +67278 Bockenheim an der Weinstraße +67280 Ebertsheim +67592 Flörsheim-Dalsheim +67590 Monsheim +67598 Gundersheim +67591 Offstein +67816 Dreisen, Standenbühl +67311 Tiefenthal +67281 Kirchheim an der Weinstraße +67246 Dirmstein +67229 Gerolsheim +67574 Osthofen +67599 Gundheim +67550 Worms +67258 Heßheim +67240 Bobenheim-Roxheim +67551 Worms +67259 Beindersheim +67547 Worms +67593 Westhofen, Bermersheim +67595 Bechtheim +67549 Worms +55286 Wörrstadt +55435 Gau-Algesheim +55291 Saulheim +55239 Gau-Odernheim +55578 Wallertheim +55288 Armsheim +55271 Stadecken-Elsheim +55232 Alzey +55268 Nieder-Olm +55237 Flonheim +67586 Hillesheim +67596 Dittelsheim-Heßloch +67575 Eich +67583 Guntersblum +67577 Alsheim +67585 Dorn-Dürkheim +67578 Gimbsheim +55129 Mainz Ebersheim, Hechtsheim +55296 Harxheim +55276 Oppenheim +55283 Nierstein +55299 Nackenheim +55294 Bodenheim +55278 Mommenheim +67587 Wintersheim +67582 Mettenheim +67166 Otterstadt +67141 Neuhofen +67165 Waldsee +67122 Altrip +67059 Ludwigshafen am Rhein +67061 Ludwigshafen am Rhein +67580 Hamm +54531 Manderscheid +54533 Bettenfeld, Niederöfflingen u.a. +54570 Pelm, Neroth u.a. +54597 Pronsfeld +54619 Üttfeld +54617 Lützkampen +54673 Neuerburg u.a. +54689 Daleiden, Preischeid u.a. +54687 Arzfeld +54608 Bleialf +54616 Winterspelt +54614 Schönecken +54612 Lasel +54649 Waxweiler +54526 Landscheid +54647 Dudeldorf +54655 Kyllburg +54657 Badem, Gindorf, Neidenbach +54529 Spangdahlem +54611 Hallschlag +54595 Prüm +54589 Stadtkyll +54574 Birresborn +54584 Jünkerath +54579 Üxheim +54585 Esch +54576 Hillesheim +54568 Gerolstein +54610 Büdesheim +54578 Walsdorf, Nohn u.a. +54587 Lissendorf +54586 Schüller +54516 Wittlich +54534 Großlittgen +54558 Gillenfeld +54552 Mehren u.a. +56288 Kastellaun +56290 Beltheim +56858 Peterswald-Löffelscheid u.a. +53506 Ahrbrück, Heckenbach, Hönningen, Kesseling, Rech +56825 Gevenich +56861 Reil +56856 Zell (Mosel) +56812 Cochem +56864 Bad Bertrich +56859 Bullay, Alf, Zell +56867 Briedel +56826 Lutzerath +56850 Enkirch u.a. +56862 Pünderich +56814 Ediger-Eller +54492 Zeltingen-Rachtig, Erden, Lösnich u.a. +54536 Kröv +54538 Bausendorf +54539 Ürzig +56767 Uersfeld +54550 Daun +53520 Reifferscheid, Kaltenborn, Wershofen u.a. +53539 Bodenbach, Kelberg, Kirsbach u.a. +53518 Adenau, Kottenborn u.a. +53534 Barweiler, Bauler, Hoffeld, Pomster, Wiesemscheid, Wirft +56727 Mayen +56759 Kaisersesch +56729 Ettringen +56761 Düngenheim +56769 Retterath +56766 Ulmen +56828 Alflen +56823 Büchel +56253 Treis-Karden +56865 Blankenrath u.a. +56820 Senheim +56869 Mastershausen +56821 Ellenz-Poltersdorf +56281 Emmelshausen +56291 Leiningen +56283 Wildenbungert, Gondershausen, Nörtershausen u.a. +55469 Simmern/Hunsrück u.a. +56294 Münstermaifeld +56751 Polch +56753 Mertloch, Welling u.a. +56754 Binningen +56743 Mendig +56818 Klotten +56829 Pommern +56254 Müden +56736 Kottenheim +56299 Ochtendung +56073 Koblenz +56072 Koblenz +56075 Koblenz +56295 Lonnig +56220 Urmitz +56332 Lehmen, Niederfell, Oberfell, Wolken u.a. +56330 Kobern-Gondorf +56154 Boppard +56323 Waldesch, Hünenfeld +56333 Winningen +53533 Antweiler, Aremberg, Dorsel, Eichenbach, Aremberg, Fuchshofen und Müsch +53505 Altenahr, Berg, Kalenborn, Kirchsahr +56651 Niederzissen +56745 Bell +56746 Kempenich +53426 Schalkenbach, Königsfeld, Dedenbach +53501 Grafschaft +53474 Bad Neuenahr-Ahrweiler +53507 Dernau +53508 Mayschoß +56598 Rheinbrohl +56653 Wehr +53547 Breitscheid, Dattenberg, Hausen, Hümmerich, Kasbach-Ohlenberg, Roßbach u.a. +53545 Linz am Rhein, Ockenfels +53498 Bad Breisig, Waldorf, Gönnersdorf +53424 Remagen +53489 Sinzig +53557 Bad Hönningen +56656 Brohl-Lützing +56659 Burgbrohl +56626 Andernach +56645 Nickenich +56642 Kruft +56630 Kretz +56070 Koblenz +56317 Urbach +56307 Dürrholz +56587 Straßenhaus +56564 Neuwied +56566 Neuwied +56589 Niederbreitbach +56567 Neuwied +56581 Melsbach +56584 Anhausen +56579 Rengsdorf +56588 Waldbreitbach, Hasuen +56599 Leutesdorf +56637 Plaidt +56218 Mülheim-Kärlich +56575 Weißenthurm +56648 Saffig +53560 Vettelschloß, Kretzhaus (Linz am Rhein) +53572 Bruchhausen, Unkel +53562 Sankt Katharinen (Landkreis Neuwied) +53578 Windhagen +53579 Erpel +53619 Rheinbreitbach +57635 Weyerbusch +57632 Flammersfeld +57641 Oberlahr +56305 Puderbach +56593 Horhausen (Westerwald) +53567 Asbach, Buchholz +53577 Neustadt (Wied) +56594 Willroth +56348 Bornich, Patersberg +56349 Kaub +56329 Sankt Goar +56346 Sankt Goarshausen u.a. +55422 Bacharach, Breitscheid +55413 Weiler bei Bingen +55494 Rheinböllen +55430 Oberwesel +55432 Niederburg +55497 Ellern (Hunsrück), Schnorbach +56379 Singhofen +56357 Miehlen u.a. +56412 Nentershausen, Hübingen, Niederelbert u.a. +55425 Waldalgesheim +65391 Lorch +55424 Münster-Sarmsheim +56077 Koblenz +56076 Koblenz +56132 Bad Ems Umland +56130 Bad Ems +56340 Osterspai +56355 Nastätten u.a. +56321 Rhens +56341 Kamp-Bornhofen-Filsen +56377 Nassau +56112 Lahnstein +56338 Braubach +56322 Spay +56068 Koblenz +56133 Fachbach, Exklave Lahnstein +56368 Katzenelnbogen +56370 Schönborn +65558 Burgschwalbach +65582 Diez, Hambach, Aull +55127 Mainz +55126 Mainz +55218 Ingelheim am Rhein +55262 Heidesheim am Rhein +55263 Wackernheim +55257 Budenheim +55120 Mainz +55122 Mainz +55131 Mainz +55128 Mainz +55130 Mainz +55124 Mainz +55118 Mainz +55116 Mainz +65629 Niederneisen +65623 Hahnstätten u.a. +65626 Birlenbach +56179 Vallendar +56206 Hilgert +56276 Großmaischeid +56237 Nauort +56235 Ransbach-Baumbach +56244 Freilingen, Freirachdorf u.a. +56242 Selters (Westerwald) +56271 Kleinmaischeid +56269 Dierdorf +56337 Eitelborn +56316 Raubach +56424 Mogendorf, Ebernhahn, Staudt u.a. +56249 Herschbach +56170 Bendorf +56191 Weitersburg +56410 Montabaur +56428 Dernbach (Westerwald) +56203 Höhr-Grenzhausen +56204 Hillscheid +56422 Wirges, Stadt +56182 Urbar (bei Koblenz) +56335 Neuhäusel +56427 Siershahn +56459 Bellingen, Kölbingen, Gemünden u.a. +56414 Meudt, Molsberg, Hundsangen, Niederahr u.a. +56457 Westerburg +65624 Altendiez +57612 Birnbach +57636 Mammelzen +57644 Hattert +57614 Steimel +57627 Hachenburg +57639 Oberdreis +57629 Malberg, Norken, Höchstenbach u.a. +57610 Altenkirchen (Westerwald) +57539 Fürthen +57589 Pracht +57537 Wissen, Hövels u.a. +57638 Neitersen +57577 Hamm (Sieg) +57647 Nistertal, Enspel +57648 Unnau +57562 Herdorf +57520 Emmerzhausen, Niederdreisbach, Steinebach +57567 Daaden +57583 Nauroth +57518 Betzdorf +56472 Nisterau u.a. +56462 Höhn +56470 Bad Marienberg (Westerwald) +57578 Elkenroth +57586 Weitefeld +57642 Alpenrod +57645 Nister +57580 Gebhardshain +56479 Oberrod u.a. +56477 Rennerod, Zehnhausen, Nister-Möhrendorf, Waigandshain +57587 Birken-Honigsessen +51598 Friesenhagen +57548 Kirchen (Sieg) +57572 Niederfischbach +57555 Mudersbach +57080 Siegen +57581 Katzwinkel (Sieg) +57584 Scheuerfeld +27498 Helgoland +25946 Amrum +25997 Hörnum (Sylt) +25980 Sylt +25996 Wenningstedt-Braderup (Sylt) +25999 Kampen (Sylt) +25767 Albersdorf +25704 Meldorf +25718 Friedrichskoog +25761 Büsum +25724 Neufeld, Schmedeswurth +25572 Sankt Margarethen +25541 Brunsbüttel +25693 Sankt Michaelisdonn,Gudendorf,Volsemenhusen,Trennewurth +25709 Kronprinzenkoog, Marne u.a. +25719 Barlt, Busenwurth +25715 Eddelak, Averlak, Dingen, Ramhusen +25785 Nordhastedt +25727 Süderhastedt +25712 Burg (Dithmarschen) +25729 Windbergen +24576 Bad Bramstedt +24568 Kaltenkirchen +23812 Wahlstedt +25548 Kellinghusen +25563 Wrist +25524 Itzehoe +22880 Wedel +25451 Quickborn +25377 Kollmar, Pagensand +25492 Heist +25436 Uetersen +25489 Haseldorf +25491 Hetlingen +25370 Seester +25371 Seestermühe +22113 Hamburg, Oststeinbek +25488 Holm +25482 Appen +25474 Ellerbek +25494 Borstel-Hohenraden +25499 Tangstedt +22869 Schenefeld +25421 Pinneberg +25462 Rellingen +25469 Halstenbek +25497 Prisdorf +25495 Kummerfeld +22844 Norderstedt +22846 Norderstedt +22848 Norderstedt +22851 Norderstedt +22457 Hamburg +22889 Tangstedt +22850 Norderstedt +25554 Wilster +25376 Borsfleth +25569 Kremperheide +25348 Glückstadt +25573 Beidenfleth, Klein Kampen +25576 Brokdorf +25599 Wewelsfleth +25597 Westermoor +25578 Dägeling, Neuenbrook +25361 Krempe, Grevenkop, Süderau, Muchelndorf +25358 Horst +25566 Lägerdorf +25364 Brande-Hörnerkirchen +25336 Elmshorn +25335 Elmshorn +25379 Herzhorn, Kamerlanderdeich +25368 Kiebitzreihe +25587 Münsterdorf +25588 Oldendorf +25582 Hohenaspe +25596 Wacken +25560 Schenefeld +25594 Vaale +25584 Holstenniendorf +25557 Hanerau-Hademarschen, Seefeld u.a. +25721 Eggstedt +25725 Schafstedt, Weidenhof, Bornholt +24594 Hohenwestedt +24819 Todenbüttel +25593 Reher +25551 Hohenlockstedt +25575 Beringstedt +25585 Lütjenwestedt, Tackesdorf +25591 Ottenbüttel +25590 Osterstedt +24632 Lentföhrden +25355 Barmstedt +25485 Hemdingen +25337 Elmshorn +25373 Ellerhoop +25365 Klein Offenseth-Sparrieshoop +24643 Struvenhütten +24558 Henstedt-Ulzburg +24641 Sievershütten +24640 Schmalfeld +24629 Kisdorf +24628 Hartenholm +25479 Ellerau +25486 Alveslohe +24649 Wiemersdorf +24622 Gnutz +24634 Padenstedt +24647 Wasbek +24613 Aukrug, Wiedenborstel +24644 Timmaspe +24616 Brokstedt +25579 Fitzbek +25581 Hennstedt +24598 Boostedt +24623 Großenaspe +24625 Großharrie +24539 Neumünster +24537 Neumünster +24620 Bönebüttel +24626 Groß Kummerfeld +24536 Neumünster +24534 Neumünster +25826 Sankt Peter-Ording +25923 Süderlügum, Braderup u.a. +24980 Schafflund, Meyn u.a. +25842 Langenhorn, Ockholm u.a. +25836 Garding, Osterhever, Poppenbüll u.a. +25845 Nordstrand, Elisabeth-Sophien-Koog, Südfall +25881 Tating, Westerhever, Tümlauer Koog +25849 Süderoog, Pellworm +25859 Hallig Hooge +25882 Tetenbüll +25870 Oldenswort +25832 Tönning +25774 Lunden +25792 Neuenkirchen +25764 Wesselburen +25746 Heide u.a. +25776 Sankt Annen, Rehm-Flehde-Bargen +25797 Wöhrden +25795 Weddingstedt +25779 Hennstedt +25788 Delve +25878 Drage, Seeth +25791 Linden, Barkenholm +25782 Tellingstedt +25770 Hemmingstedt +25879 Stapel +25813 Husum, Schwesing u.a. +25856 Hattstedt u.a. +25889 Uelvesbüll, Witzwort +25872 Ostenfeld +25873 Rantrum +25887 Winnert +25850 Behrendorf, Bondelum +25860 Horstedt +25885 Wester-Ohrstedt +25840 Friedrichstadt, Koldenbüttel u.a. +25876 Schwabstedt +25866 Mildstedt +25938 Föhr +25863 Langeneß +25920 Risum-Lindholm, Stedesand +25899 Niebüll +25869 Habel, Gröde +25867 Oland +25924 Rodenäs +25927 Neukirchen, Aventoft +25917 Leck +25852 Bordelum +25853 Ahrenshöft +25821 Bredstedt, Breklum u.a. +24992 Jörl +24969 Großenwiehe, Lindewitt +25884 Viöl +25862 Joldelund +25855 Haselund +25858 Högel +25864 Löwenstedt +25926 Ladelund +24994 Medelby +24799 Meggerdorf, Friedrichsholm, Friedrichsgraben u.a. +24803 Erfde +25794 Pahlen +25799 Wrohm +25786 Dellstedt +24805 Hamdorf +24806 Hohn +24800 Elsdorf-Westermühlen +24369 Waabs +24351 Damp +24848 Kropp u.a. +24855 Bollingstedt, Jübek +24983 Handewitt +24817 Tetenhusen +24881 Nübel +24808 Jevenstedt +24816 Hamweddel +24784 Westerrönfeld +24797 Breiholz, Tackesdorf-Nord +24813 Schülp bei Rendsburg +24809 Nübbel +24787 Fockbek +24782 Büdelsdorf, Rickert +24768 Rendsburg +24850 Schuby +24872 Groß Rheide +24861 Bergenhusen +24863 Börm +24896 Treia, Ahrenviölfeld +24899 Wohlde +24867 Dannewerk +24869 Dörpstedt +24870 Ellingstedt +24876 Hollingstedt +24887 Silberstedt, Schwittschau +24791 Alt Duvenstedt +24811 Owschlag u.a. +24357 Fleckeby u.a. +24884 Selk, Geltdorf, Hahnekrug +24857 Fahrdorf +24878 Jagel, Lottorf +24837 Schleswig +24866 Busdorf +24882 Schaalby, Geelbek +24259 Westensee +24793 Bargstedt, Brammer, Oldenbüttel +24783 Osterrönfeld +24589 Nortorf +24646 Warder +24802 Emkendorf +24794 Borgstedt +24796 Bredenbek +24790 Schacht-Audorf +24220 Flintbek +24582 Bordesholm +24241 Blumenthal +24247 Mielkendorf +24254 Rumohr +24242 Felde +24239 Achterwehr +24631 Langwedel +24119 Kronshagen +24107 Kiel +24113 Kiel +24109 Melsdorf +24111 Kiel Russee +24363 Holtsee +24814 Sehestedt +24358 Ascheffel +24361 Groß Wittensee +24354 Kosel, Rieseby u.a. +24360 Barkelsby +24366 Loose +24364 Holzdorf +24340 Eckernförde +24864 Brodersby, Goltoft +24897 Ulsnis +24367 Osterby +24161 Altenholz +24251 Osdorf +24244 Felm +24229 Dänischenhagen +24214 Gettorf u.a. +24852 Eggebek, Langstedt, Sollerup, Süderhackstedt +24963 Tarp +24941 Flensburg +24976 Handewitt +24988 Oeversee +24997 Wanderup +24885 Sieverstedt +24991 Großsolt +24975 Husby +24986 Mittelangeln +24891 Struxdorf, Schnarup-Thumby +24860 Böklund u.a. +24890 Stolk +24894 Tolk, Twedt +24879 Idstedt +24943 Flensburg, Tastrup +24966 Sörup +24873 Havetoft +24937 Flensburg +24955 Harrislee +24939 Flensburg +24960 Glücksburg, Munkbrarup +24977 Langballig +24944 Flensburg +24989 Dollerup +24999 Wees +24888 Steinfeld +24405 Mohrkirch, Rügge +24392 Süderbrarup +24407 Rabenkirchen-Faulück +24395 Gelting +24996 Sterup +24972 Steinberg, Steinbergkirche +24893 Taarstedt +24409 Stoltebüll +24401 Böel +24402 Esgrus +24398 Dörphof +24376 Kappeln +24404 Maasholm, Schleimünde +24399 Arnis, Marienhof +25992 List +21039 Hamburg +21465 Reinbek +23701 Eutin, Süsel +23623 Ahrensbök +23845 Seth +21521 Aumühle +21514 Bröthen +23883 Sterley +23911 Ziethen +22946 Trittau +22941 Bargteheide, Delingsdorf u.a. +23617 Stockelsdorf +21502 Geesthacht +21481 Lauenburg/Elbe +21483 Gülzow +21493 Fuhlenhagen +21524 Brunstorf +21529 Kröppelshagen-Fahrendorf +21526 Hohenhorn +21527 Kollow +22145 Hamburg +21509 Glinde +22926 Ahrensburg +22949 Ammersbek +22962 Siek +22885 Barsbüttel +22927 Großhansdorf +23847 Lasbek +23898 Sandesneben u.a. +22929 Hamfelde, Kasseburg, Köthel, Rausdorf, Schönberg +22955 Hoisdorf +22961 Hoisdorf +22952 Lütjensee +22964 Steinburg +22965 Todendorf +22956 Grönwohld +22958 Kuddewörde +22969 Witzhave +22959 Linau +21516 Woltersdorf, Müssen u.a. +23896 Nusse +23909 Ratzeburg +23899 Gudow +23881 Breitenfelde, Lankau +23919 Berkenthin +23879 Mölln +23826 Bark +23795 Bad Segeberg +23829 Wittenborn +23863 Bargfeld-Stegen +23843 Bad Oldesloe +23816 Leezen +23866 Nahe +23867 Sülfeld +23869 Elmenhorst +22967 Tremsbüttel +23815 Geschendorf +23858 Reinfeld (Holstein) +23619 Zarpen +23818 Neuengörs +23820 Pronstorf +24326 Ascheberg +24638 Schmalensee +24601 Wankendorf +24619 Börnhöved +24610 Trappenkamp +24635 Rickling +24637 Schillsdorf +24306 Plön +23813 Nehms +23824 Tensfeld +23827 Wensin +23715 Bosau +23719 Glasau +23821 Rohlstorf +23823 Seedorf +23628 Krummesse, Klempau +23627 Groß Grönau +23860 Klein Wesenberg +23611 Bad Schwartau +23568 Lübeck Schlutup/St. Gertrud +23556 Lübeck +23560 Lübeck +23562 Lübeck +23564 Lübeck +23558 Lübeck +23552 Lübeck +23554 Lübeck St. Lorenz Nord +23566 Lübeck +23569 Lübeck +23570 Lübeck +23684 Scharbeutz, Süsel +23689 Pansdorf +23717 Kasseedorf +23629 Sarkwitz +23730 Neustadt +23683 Scharbeutz +23626 Ratekau +23669 Timmendorfer Strand +24223 Schwentinetal +24211 Preetz +24232 Schönkirchen +24245 Kirchbarkau +24250 Nettelsee +24149 Kiel +24145 Kiel +24146 Kiel +23758 Oldenburg in Holstein +24257 Hohenfelde +24321 Lütjenburg +23738 Lensahn +23714 Malente, Kirchnüchel +23743 Grömitz +24116 Kiel +24118 Kiel +24114 Kiel +24105 Kiel +24103 Kiel +24147 Kiel +24143 Kiel +24148 Kiel +24222 Schwentinental +24329 Grebin +24256 Fargau-Pratjau +24238 Selent +24106 Kiel +24253 Probsteierhagen +24235 Laboe +24226 Heikendorf +24159 Kiel +24248 Mönkeberg +24217 Schönberg (Holstein) +24327 Blekendorf +23744 Schönwalde am Bungsberg +23777 Heringsdorf +23749 Grube +23747 Dahme +23769 Fehmarn +23779 Neukirchen +23746 Kellenhusen +23775 Großenbrode +23774 Heiligenhafen +66740 Saarlouis +66798 Wallerfangen +66802 Überherrn +66706 Perl +66693 Mettlach +66679 Losheim +66701 Beckingen +66663 Merzig +66780 Rehlingen-Siersburg +66763 Dillingen/Saar +66359 Bous +66115 Saarbrücken +66126 Saarbrücken +66127 Saarbrücken +66787 Wadgassen +66333 Völklingen +66352 Großrosseln +66806 Ensdorf +66773 Schwalbach +66346 Püttlingen +66292 Riegelsberg +66128 Saarbrücken +66583 Spiesen-Elversberg +66271 Kleinblittersdorf +66121 Saarbrücken +66123 Saarbrücken +66131 Saarbrücken +66119 Saarbrücken +66113 Saarbrücken +66130 Saarbrücken +66133 Saarbrücken +66287 Quierschied +66280 Sulzbach/Saar +66386 Sankt Ingbert +66399 Mandelbachtal +66129 Saarbrücken +66117 Saarbrücken +66111 Saarbrücken +66125 Saarbrücken +66132 Saarbrücken +66571 Eppelborn +66265 Heusweiler +66793 Saarwellingen +66822 Lebach +66809 Nalbach +66839 Schmelz +66636 Tholey +66709 Weiskirchen +66687 Wadern +66578 Schiffweiler +66564 Ottweiler +66589 Merchweiler +66540 Neunkirchen +66557 Illingen +66646 Marpingen +66606 Sankt Wendel +66640 Namborn +66649 Oberthal +66299 Friedrichsthal +66539 Neunkirchen +66453 Gersheim +66440 Blieskastel +66459 Kirkel +66424 Homburg +66538 Neunkirchen +66450 Bexbach +66629 Freisen +66620 Nonnweiler +66625 Nohfelden +07952 Pausa-Mühltroff +08606 Oelsnitz +08626 Adorf +08261 Schöneck/Vogtl. +08267 Klingenthal +08645 Bad Elster +08648 Bad Brambach +08258 Markneukirchen +08248 Klingenthal/Sa. +07919 Kirschkau, Pausa-Mühltroff +08538 Weischlitz u.a. +08539 Rosenbach +08543 Pöhl +08547 Jößnitz +08548 Rosenbach +08527 Plauen, Rößnitz +08525 Plauen +08523 Plauen +08529 Plauen +07985 Elsterberg +08541 Neuensalz +08209 Auerbach/Vogtl. +08228 Rodewisch +08236 Ellefeld +08223 Falkenstein/Vogtl. +08233 Treuen +08239 Bergen +08485 Lengenfeld +08147 Crinitzberg +08107 Kirchberg +08262 Muldenhammer +08237 Steinberg +08304 Schönheide +08328 Stützengrün +08309 Eibenstock +08428 Langenbernsdorf +08427 Fraureuth +08412 Werdau +08459 Neukirchen/Pleiße +08468 Reichenbach/Vogtl. +08496 Neumark +08491 Netzschkau, Limbach +08499 Mylau +08115 Lichtentanne +08371 Glauchau +08112 Wilkau-Haßlau +08058 Zwickau +08060 Zwickau +08144 Hirschfeld +08056 Zwickau +08134 Langenweißbach, Wildenfels +08141 Reinsdorf +08132 Mülsen +08066 Zwickau +08289 Schneeberg +08064 Zwickau +08062 Zwickau +09619 Dorfchemnitz, Mulda, Sayda +08344 Grünhain-Beierfeld +08321 Zschorlau +08359 Breitenbrunn/Erzgeb. +08340 Schwarzenberg/Erzgeb. +08349 Johanngeorgenstadt +08315 Lauter-Bernsbach +08324 Bockau +09487 Schlettau +09481 Scheibenberg +09484 Oberwiesenthal +09456 Annaberg-Buchholz, Mildenau +09465 Sehma +09474 Crottendorf +08352 Raschau +08118 Hartenstein +09337 Callenberg, Hohenstein-Ernstthal, Bernsdorf +09356 St. Egidien +09387 Jahnsdorf +09385 Lugau/Erzgeb. +09366 Stollberg/Erzgeb. +09399 Niederwürschnitz +09376 Oelsnitz/Erzgebirge +08297 Zwönitz +08294 Lößnitz +08301 Schlema +08280 Aue +09350 Lichtenstein +09355 Gersdorf +09394 Hohndorf +09123 Chemnitz +09439 Amtsberg +09488 Wiesa +09235 Burkhardtsdorf +09468 Geyer +09427 Ehrenfriedersdorf +09419 Thum +09423 Gelenau/Erzgeb. +09392 Auerbach +09221 Neukirchen/Erzgeb. +09430 Drebach +09380 Thalheim/Erzgebirge +09390 Gornsdorf +09125 Chemnitz +09477 Jöhstadt +09496 Marienberg +09471 Bärenstein +09573 Leubsdorf, Gornau, Augustusburg +09575 Eppendorf +09432 Großolbersdorf +09429 Wolkenstein +09437 Börnichen, Gornau +09514 Pockau-Lengefeld (Lengefeld) +09579 Grünhainichen +09509 Pockau-Lengefeld (Pockau) +09518 Großrückerswalde +09405 Zschopau, Gornau +09434 Zschopau +09618 Brand-Erbisdorf, Großhartmannsdorf +09548 Seiffen/Erzgeb. +09526 Olbernhau, Pfaffroda, Heidersdorf +04654 Frohburg +04651 Bad Lausick +04668 Grimma +04509 Delitzsch, Krostitz u.a. +04838 Eilenburg u.a. +08451 Crimmitschau +08393 Meerane +08373 Remse +08396 Waldenburg +04613 Lucka +04539 Groitzsch +04565 Regis-Breitingen +04564 Böhlen +04575 Neukieritzsch, Deutzen +04523 Pegau, Elstertrebnitz +04442 Zwenkau +04552 Borna +04571 Rötha +04567 Kitzscher +04420 Markranstädt +04416 Markkleeberg +04129 Leipzig +04159 Leipzig +04179 Leipzig +04249 Leipzig +04277 Leipzig +04207 Leipzig +04105 Leipzig +04178 Leipzig +04435 Schkeuditz +04158 Leipzig +04205 Leipzig +04229 Leipzig +04209 Leipzig +04177 Leipzig +04109 Leipzig +04107 Leipzig +04275 Leipzig +04157 Leipzig +04155 Leipzig +04821 Brandis +04451 Borsdorf +04827 Machern +04463 Großpösna +04683 Naunhof +04318 Leipzig +04356 Leipzig +04288 Leipzig +04103 Leipzig +04319 Leipzig +04328 Leipzig +04349 Leipzig +04347 Leipzig +04289 Leipzig +04425 Taucha +04279 Leipzig +04317 Leipzig +04299 Leipzig +04315 Leipzig +04316 Leipzig +04357 Leipzig +04329 Leipzig +04824 Brandis +04519 Rackwitz +04849 Bad Düben +09212 Limbach-Oberfrohna +09224 Chemnitz +09328 Lunzenau +09217 Burgstädt +09322 Penig +09232 Hartmannsdorf +09241 Mühlau +09243 Niederfrohna +09353 Oberlungwitz +09600 Weißenborn, Oberschöna +09661 Hainichen, Rossau, Striegistal +09306 Rochlitz +09669 Frankenberg +04808 Wurzen +04880 Dommitzsch +04774 Dahlen +04758 Oschatz +04886 Arzberg, Beilrode +04769 Mügeln +01609 Gröditz, Wülknitz, Röderaue +01561 Großenhain, Ebersbach u.a. +01665 Käbschütztal, Klipphausen, Diera-Zehren +01594 Riesa, Stauchitz, Hirschstein +01623 Lommatzsch +01723 Wilsdruff +04720 Döbeln, Großweitzschen u.a. +04703 Leisnig +04749 Ostrau +04862 Mockrehna +09114 Chemnitz +09228 Chemnitz +09247 Röhrsdorf +09117 Chemnitz +09128 Chemnitz +09126 Chemnitz +09113 Chemnitz +09127 Chemnitz +09131 Chemnitz +09116 Chemnitz +09249 Taura b. Burgstädt +09577 Niederwiesa +09236 Claußnitz +09244 Lichtenau +09648 Mittweida, Kriebstein +09120 Chemnitz +09119 Chemnitz +09122 Chemnitz +09112 Chemnitz +09111 Chemnitz +09130 Chemnitz +04643 Geithain +04680 Colditz +09326 Geringswalde +04736 Waldheim +04746 Hartha +09603 Großschirma +09557 Flöha +09569 Oederan +09633 Halsbrücke +09599 Freiberg +09638 Lichtenberg/Erzgeb. +09627 Bobritzsch +04741 Roßwein +09629 Reinsberg +01683 Nossen +09634 Reinsberg +04687 Trebsen/Mulde +04828 Bennewitz, Machern +04779 Wermsdorf +04860 Torgau, Dreiheide +04889 Belgern-Schildau +04861 Torgau +01616 Strehla +01591 Riesa +01589 Riesa +01587 Riesa +01619 Zeithain +01612 Nünchritz, Glaubitz +04874 Belgern-Schildau +09544 Neuhausen/Erzgeb. +09623 Frauenstein +01776 Hermsdorf/Erzgeb. +01773 Altenberg +01778 Altenberg +01774 Klingenberg +01738 Dorfhain +01737 Tharandt u.a. +01734 Rabenau +01762 Hartmannsdorf-Reichenau +01705 Freital +01689 Weinböhla +01471 Radeburg +01847 Lohmen +01744 Dippoldiswalde +01936 Königsbrück u.a. +01920 Elstra, Oßling u.a. +02991 Lauta +02979 Spreetal, Elsterheide +02699 Königswartha +02994 Bernsdorf +01257 Dresden +01825 Liebstadt +01809 Heidenau +01731 Kreischa +01768 Glashütte +01728 Bannewitz +01445 Radebeul +01640 Coswig +01468 Moritzburg +01662 Meißen +01156 Dresden +01169 Dresden +01108 Dresden +01069 Dresden +01129 Dresden +01328 Dresden +01277 Dresden +01326 Dresden +01189 Dresden +01259 Dresden +01187 Dresden +01465 Dresden +01109 Dresden +01099 Dresden +01159 Dresden +01139 Dresden +01458 Ottendorf-Okrilla +01217 Dresden +01157 Dresden +01067 Dresden +01127 Dresden +01097 Dresden +01239 Dresden +01237 Dresden +01219 Dresden +01279 Dresden +01309 Dresden +01307 Dresden +01324 Dresden +01848 Hohnstein +01824 Königstein/Sächs.Schw. +01819 Bahretal +01816 Bad Gottleuba-Berggießhübel +01829 Wehlen +01796 Pirna, Struppen, Dohma +01844 Neustadt i. Sa. +01814 Bad Schandau +01855 Sebnitz +01833 Stolpen, Dürrröhrsdorf-Dittersbach +01477 Arnsdorf b. Dresden +01896 Pulsnitz +01454 Radeberg, Wachau +01900 Großröhrsdorf, Bretnig-Hauswalde +01909 Großharthau, Frankenthal +01906 Burkau +01877 Bischofswerda u.a. +02633 Göda +01558 Großenhain +01917 Kamenz +02977 Hoyerswerda +02997 Wittichenau +02999 Lohsa +02627 Weißenberg, Hochkirch u.a. +02943 Weißwasser, Boxberg +02906 Niesky, Hohendubrau u.a. +02894 Reichenbach, Vierkirchen +02829 Markersdorf, Neißeaue u.a. +02957 Krauschwitz, Weißkeißel +02727 Ebersbach-Neugersdorf +02747 Herrnhut +02799 Großschönau +02794 Leutersdorf, Spitzkunnersdorf +02791 Oderwitz +02739 Kottmar +02796 Jonsdorf +02782 Seifhennersdorf +02779 Großschönau +01904 Neukirch/Lausitz +02692 Doberschau-Gaußig, Großpostwitz, Obergurig +02689 Sohland a. d. Spree +02681 Wilthen +02625 Bautzen +02733 Cunewalde +02730 Ebersbach-Neugersdorf +02708 Löbau, Kottmar u.a. +02742 Neusalza-Spremberg +02736 Beiersdorf, Oppach +02797 Oybin +02788 Zittau +02785 Olbersdorf +02899 Ostritz, Schönau-Berzdorf +02763 Zittau u.a. +02748 Bernstadt a. d. Eigen +02828 Görlitz +02826 Görlitz +02827 Görlitz +02694 Großdubrau, Malschwitz +02959 Schleife +02953 Bad Muskau, Groß Düben, Gablenz +02923 Hähnichen, Horka, Kodersdorf +02956 Rietschen +02929 Rothenburg/O.L. +06493 Ballenstedt, Harzgerode +06536 Südharz, Berga +06268 Querfurt, Obhausen, Mücheln u.a. +06542 Allstedt +06526 Sangerhausen +06528 Wallhausen, Blankenheim +06618 Naumburg +06246 Bad Lauchstädt +06179 Teutschenthal +06347 Gerbstedt +06343 Mansfeld +06193 Petersberg +06648 Eckartsberga +06647 Bad Bibra, Finne u.a. +06628 Lanitz-Hassel-Tal, Molauer Land +06632 Freyburg, Balgstädt +06571 Roßleben-Wiehe, Gehofen +06537 Kelbra (Kyffhäuser) +06642 Nebra, Kaiserpfalz +06636 Laucha an der Unstrut +06249 Mücheln/ Geiseltal +06638 Karsdorf +06279 Schraplau, Farnstädt +06295 Eisleben +06311 Helbra +06313 Hergisdorf +06308 Klostermansfeld, Benndorf +06317 Seegebiet Mansfelder Land +06198 Salzatal +06722 Droyßig, Wetterzeube +06258 Schkopau +06774 Muldestausee +06712 Zeitz, Gutenborn u.a. +06721 Meineweh, Osterfeld +06682 Teuchern +06667 Weißenfels, Stößen +06711 Zeitz +06729 Elsteraue +06679 Hohenmölsen +06686 Lützen +06242 Braunsbedra +06259 Frankleben +06217 Merseburg +06255 Mücheln +06688 Weißenfels +06231 Bad Dürrenberg +06237 Leuna +06110 Halle/ Saale +06132 Halle/ Saale +06120 Halle/ Saale +06118 Halle/ Saale +06128 Halle/ Saale +06114 Halle/ Saale +06108 Halle/ Saale +06126 Halle/ Saale +06124 Halle/ Saale +06122 Halle/ Saale +06780 Zörbig +06794 Sandersdorf-Brehna +06130 Halle/ Saale +06116 Halle/ Saale +06112 Halle/ Saale +06184 Kabelsketal +06188 Landsberg +06809 Roitzsch, Petersroda +06749 Bitterfeld-Wolfen +06808 Bitterfeld-Wolfen +06792 Sandersdorf-Brehna +06796 Sandersdorf-Brehna +06502 Thale, Blankenburg +38889 Blankenburg, Oberharz am Brocken +38855 Wernigerode, Nordharz +38871 Ilsenburg, Nordharz +38822 Halberstadt, Groß Quenstedt +38875 Oberharz am Brocken +38877 Oberharz am Brocken +38879 Wernigerode +38899 Oberharz am Brocken +38835 Osterwieck +38836 Badersleben u.a. +38895 Langenstein, Derenburg +06485 Quedlinburg, Ballenstedt +39291 Möckern, Schermen, Nedlitz u.a. +39517 Tangerhütte u.a. +39638 Gardelegen +39343 Erxleben, Nordgermersleben u.a. +39365 Harbke, Sommersdorf, Wefensleben, Ummendorf, Eilsleben +39326 Wolmirstedt u.a. +39164 Wanzleben-Börde +39397 Schwanebeck, Gröningen, Kroppenstedt +39345 Haldensleben, Flechtingen, Bülstringen u.a. +39649 Gardelegen +39359 Rätzlingen, Wegenstedt, Calvörde, Böddensell u.a. +06484 Quedlinburg +06456 Arnstein +06449 Aschersleben +39240 Calbe, Rosenburg u.a. +39288 Burg +06463 Falkenstein +06458 Hedersleben +06466 Seeland +06467 Seeland +06469 Seeland +06543 Falkenstein +39393 Hötensleben, Völpke, Ottleben u.a. +38838 Huy +38820 Halberstadt +38829 Harsleben +39387 Oschersleben (Bode) +38828 Wegeleben +39418 Staßfurt +39444 Hecklingen +06464 Seeland +06333 Hettstedt, Endorf +06406 Bernburg (Saale) +06420 Könnern +06408 Ilberstedt +06425 Alsleben/Saale, Plötzkau +06429 Nienburg (Saale) +39439 Güsten +39435 Egeln, Borne, Wolmirsleben u.a. +39446 Staßfurt +39171 Sülzetal +39448 Börde-Hakel +39443 Staßfurt +39221 Welsleben, Biere, Eickendorf u.a. +39218 Schönebeck +39217 Schönebeck (Elbe) +39122 Magdeburg +39646 Oebisfelde +39356 Weferlingen, Behnsdorf, Belsdorf u.a. +39167 Eichenbarleben, Irxleben, Niederndodeleben u.a. +39116 Magdeburg +39175 Biederitz, Gerwisch, Menz u.a. +39179 Barleben +39110 Magdeburg +39108 Magdeburg +39114 Magdeburg +39118 Magdeburg +39120 Magdeburg +39126 Magdeburg +39128 Magdeburg +39104 Magdeburg +39112 Magdeburg +39130 Magdeburg +39124 Magdeburg +39106 Magdeburg +39340 Haldensleben +38489 Beetzendorf, Rohrberg, Jübar +29413 Dähre, Diesdorf, Wallstawe +38486 Klötze, Apenburg-Winterfeld +39615 Seehausen, Werben, Leppin u.a. +39606 Osterburg, Altmärkische Höhe +39579 Rochau +39624 Kalbe +29410 Salzwedel +39576 Stendal +39619 Arendsee +39628 Bismark +29416 Kuhfelde +39629 Bismark +06366 Köthen +06388 Südliches Anhalt, Köthen +06869 Coswig (Anhalt) +06886 Wittenberg +06842 Dessau-Roßlau +06772 Gräfenhainichen +06785 Oranienbaum-Wörlitz +39264 Güterglück, Lindau, Deetz u.a. +39279 Loburg, Leitzkau +39319 Jerichow +39307 Genthin, Hohenseeden, Zabakuck u.a. +06369 Südliches Anhalt u.a. +06386 Osternienburger Land +14715 Milower Land, Schollene, Nennhausen u.a. +06901 Kemberg +06888 Wittenberg +06889 Wittenberg +06773 Gräfenhainichen +06846 Dessau-Roßlau +06847 Dessau-Roßlau +39249 Barby +06862 Dessau-Roßlau +06861 Dessau-Roßlau +39261 Zerbst/Anhalt +06385 Aken (Elbe) +06779 Raguhn-Jeßnitz +06800 Raguhn-Jeßnitz +06766 Bitterfeld-Wolfen +06803 Bitterfeld-Wolfen +06849 Dessau-Roßlau +06868 Coswig (Anhalt) +06844 Dessau-Roßlau +39245 Gommern, Dannigkow +39317 Elbe-Parey +06895 Zahna-Elster +06905 Bad Schmiedeberg +06917 Jessen (Elster) +06925 Annaburg +39590 Tangermünde +39596 Goldbeck, Arneburg u.a. +39539 Havelberg +39524 Sandau +98634 Wasungen +36404 Vacha, Unterbreizbach +36452 Kaltennordheim +36419 Geisa +99834 Gerstungen +99837 Berka/ Werra +37308 Heiligenstadt +36414 Unterbreizbach +36460 Krayenberggemeinde, Frauensee +37318 Arenshausen, Uder u.a. +98663 Bad Colberg-Heldburg +98553 Schleusingen u.a. +98617 Meiningen +98574 Schmalkalden +36433 Bad Salzungen +99330 Gräfenroda +99310 Arnstadt +99885 Luisenthal, Ohrdruf, Wolfis +98630 Römhild +98646 Hildburghausen +98530 Suhl, Marisfeld, Rohr u.a. +98631 Grabfeld +98590 Schwallungen +36466 Dermbach, Wiesenthal +36457 Stadtlengsfeld, Weilar, Urnshausen +98597 Breitungen/Werra +36456 Barchfeld-Immelborn +98639 Walldorf +98547 Viernau u.a. +98593 Floh-Seligenthal +98596 Brotterode-Trusetal +98660 Themar +98701 Großbreitenbach +98666 Biberau, Masserberg +98673 Eisfeld, Auengrund +98667 Schleusegrund +98669 Veilsdorf +98559 Oberhof +98529 Suhl +98528 Suhl +98527 Suhl +98587 Steinbach-Hallenberg +98544 Zella-Mehlis +98554 Benshausen +99887 Georgenthal/ Thür. Wald +99897 Tambach-Dietharz/ Thür. +98704 Langewiesen +98693 Ilmenau +98714 Stützerbach +98711 Schmiedefeld, Frauenwald, Suhl +98716 Geschwenda +99338 Arnstadt +07389 Ranis +07922 Tanna +07926 Gefell +07907 Schleiz +96515 Sonneberg, Judenbach +07330 Probstzella +07407 Rudolstadt +07318 Saalfeld/Saale +96528 Frankenblick, Schalkau, Bachfeld +96524 Neuhaus-Schierschnitz +96523 Steinach +98724 Neuhaus am Rennweg, Lauscha +98744 Oberweißbach u.a. +98746 Katzhütte +98743 Gräfenthal +07426 Königsee-Rottenbach u.a. +98708 Gehren +99326 Stadtilm, Ilmtal +07429 Döschnitz, Sitzendorf, Rohrbach +07427 Schwarzburg +07422 Bad Blankenburg +07368 Remptendorf +07343 Wurzbach +07338 Kaulsdorf +07349 Lehesten +07366 Rosenthal am Rennsteig +07356 Lobenstein +07929 Saalburg-Ebersdorf +07387 Krölpa +07333 Unterwellenborn +07768 Kahla +07381 Pößneck +07924 Ziegenrück +07806 Neustadt/ Orla +07646 Stadtroda u.a. +36469 Tiefenort +99817 Eisenach +99092 Erfurt +99334 Elleben, Wachsenburg +99869 Drei Gleichen +99947 Bad Langensalza +99976 Rodeberg, Dünwald u.a. +99974 Mühlhausen, Unstruttal +99820 Hörselberg-Hainich +37339 Worbis +99706 Sondershausen +99713 Ebeleben +99755 Ellrich +99734 Nordhausen +99759 Sollstedt +99819 Marksuhl, Krauthausen u.a. +99707 Kyffhäuserland +36448 Bad Liebenstein +99891 Tabarz/ Thür. Wald +99880 Waltershausen +99842 Ruhla +99848 Wutha-Farnroda +99846 Seebach +99988 Südeichsfeld +99831 Creuzburg, Ifta +99826 Mihla +99830 Treffurt +99986 Vogtei, Kammerforst u.a. +99998 Körner, Weinbergen +99867 Gotha +99894 Leinatal +99192 Nesse-Apfelstädt, Nottleben +99090 Erfurt +99958 Großvargula, Tonna +99991 Großengottern, Heroldishausen +99100 Großfahner, Dachwig u.a. +99955 Bad Tennstedt +99189 Gebesee +37359 Küllstedt +37327 Leinefelde-Worbis, Wingerode, Hausen +37351 Dingelstädt +37355 Niederorschel u.a. +37345 Am Ohmberg, Sonnenstein +99996 Menteroda, Obermehler +99994 Schlotheim +99735 Werther Hohenstein Wolkramshausen +99718 Greußen Clingen Großenehrich +99752 Bleicherode +99765 Heringen/ Helme +99768 Harztor +99098 Erfurt +99094 Erfurt +99097 Erfurt +99085 Erfurt +99099 Erfurt +99102 Rockhausen, Klettbach +99198 Udestedt, Mönchenholzhausen u.a. +99610 Sömmerda +07751 Jena, Bucha, Großpürschütz u.a. +99084 Erfurt +99096 Erfurt +99438 Bad Berka u.a. +99425 Weimar +99444 Blankenhain +99448 Kranichfeld u.a. +99423 Weimar +99428 Weimar +99195 Großrudestedt, Schloßvippach u.a. +99634 Straußfurt +99631 Weißensee +99091 Erfurt +99089 Erfurt +99095 Erfurt +99087 Erfurt +99086 Erfurt +99636 Rastenberg +99628 Buttstädt +99625 Kölleda +99439 Berlstedt +99427 Weimar +99510 Apolda +99441 Lehnstedt u.a. +07745 Jena +07778 Neuengönna u.a. +07619 Schkölen +07616 Bürgel u.a. +07743 Jena +07749 Jena +07747 Jena +99518 Bad Sulza +07774 Dornburg-Camburg u.a. +99638 Kindelbrück +06578 Bilzingsleben Kannawurf Oldisleben +06567 Bad Frankenhausen/Kyffhäuser +06571 Roßleben-Wiehe, Gehofen +06556 Artern/Unstrut u.a. +06577 Heldrungen +06537 Kelbra (Kyffhäuser) +07937 Zeulenroda-Triebes, Langenwolschendorf +07570 Weida, Harth-Pöllnitz, Wünschendorf +07580 Ronneburg, Braunichswalde, Großenstein u.a. +95183 Feilitzsch +07927 Hirschberg +07919 Kirschkau, Pausa-Mühltroff +07819 Triptis +07950 Zeulenroda-Triebes, Weißendorf +07955 Auma-Weidatal +07980 Berga/Elster +07987 Mohlsdorf-Teichwolframsdorf +07973 Greiz +07957 Langenwetzendorf +07958 Hohenleuben +07613 Crossen, Heideland u.a. +07639 Bad Klosterlausnitz +07607 Eisenberg, Gösen, Hainspitz +07629 Hermsdorf +07586 Bad Köstritz +07589 Münchenbernsdorf, Schwarzbach, Bocka +04603 Nobitz, Göhren, Windischleuba +04618 Langenleuba-Niederhain +04639 Gößnitz +07557 Gera, Zedlitz u.a. +07554 Brahmenau +07546 Gera +07548 Gera +07549 Gera +07551 Gera +07552 Gera +07545 Gera +04626 Schmölln, Altkirchen, Nöbdenitz u.a. +04600 Altenburg +04617 Rositz, Starkenberg, Treben +04610 Meuselwitz +04613 Lucka diff --git a/apps/address-validation-service/src/test/java/de/openknowledge/sample/address/AddressValidationServiceTest.java b/apps/address-validation-service/src/test/java/de/openknowledge/sample/address/AddressValidationServiceTest.java new file mode 100644 index 0000000..e9e0566 --- /dev/null +++ b/apps/address-validation-service/src/test/java/de/openknowledge/sample/address/AddressValidationServiceTest.java @@ -0,0 +1,55 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address; + +import org.apache.meecrowave.Meecrowave; +import org.apache.meecrowave.junit5.MonoMeecrowaveConfig; +import org.apache.meecrowave.testing.ConfigurationInject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import au.com.dius.pact.provider.junit5.HttpTestTarget; +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.State; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; + +@Provider("address-validation-service") +@PactFolder("src/test/pacts") +@MonoMeecrowaveConfig +public class AddressValidationServiceTest { + + @ConfigurationInject + private Meecrowave.Builder config; + + @BeforeEach + public void setUp(PactVerificationContext context) { + context.setTarget(new HttpTestTarget("localhost", config.getHttpPort(), "/")); + } + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @State("Three customers") + public void setThreeCustomers() { + // nothing to do + } +} diff --git a/apps/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json b/apps/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json new file mode 100644 index 0000000..11006e1 --- /dev/null +++ b/apps/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json @@ -0,0 +1,126 @@ +{ + "provider": { + "name": "address-validation-service" + }, + "consumer": { + "name": "delivery-service" + }, + "interactions": [ + { + "description": "POST request for Max's address", + "request": { + "method": "POST", + "path": "/valid-addresses", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "26122 Oldenburg", + "street": { + "number": "1", + "name": "Poststrasse" + }, + "recipient": "Max Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for Sherlock's address", + "request": { + "method": "POST", + "path": "/valid-addresses", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "London NW1 6XE", + "street": { + "number": "221B", + "name": "Baker Street" + }, + "recipient": "Sherlock Holmes" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/problem+json" + }, + "body": { + "detail": "Addresses from UK are not supported for delivery" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/problem\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.detail": { + "matchers": [ + { + "match": "regex", + "regex": ".*" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + } + ], + "metadata": { + "pactSpecification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "4.0.10" + } + } +} \ No newline at end of file diff --git a/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json new file mode 100644 index 0000000..7f16eca --- /dev/null +++ b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26909 Oldenburg" +} \ No newline at end of file diff --git a/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json new file mode 100644 index 0000000..4e26f97 --- /dev/null +++ b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "12345 Oldenburg" +} \ No newline at end of file diff --git a/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json new file mode 100644 index 0000000..a49a476 --- /dev/null +++ b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26121 Oldenburg" +} \ No newline at end of file diff --git a/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json new file mode 100644 index 0000000..1d5f468 --- /dev/null +++ b/apps/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 Oldenburg" +} \ No newline at end of file diff --git a/apps/billing-service/.gitignore b/apps/billing-service/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/apps/billing-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/apps/billing-service/Dockerfile b/apps/billing-service/Dockerfile new file mode 100644 index 0000000..9a8b4a3 --- /dev/null +++ b/apps/billing-service/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:11-jre + +RUN wget https://repo.maven.apache.org/maven2/org/apache/meecrowave/meecrowave-core/1.2.13/meecrowave-core-1.2.13-runner.jar -O /opt/meecrowave-core-runner.jar +ADD target/billing-service.war /opt/billing-service.war + +EXPOSE 4001 +ENTRYPOINT ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "/opt/meecrowave-core-runner.jar", "--host", "0.0.0.0", "--http", "4001", "--webapp", "/opt/billing-service.war"] diff --git a/apps/billing-service/Jenkinsfile b/apps/billing-service/Jenkinsfile new file mode 100644 index 0000000..7916245 --- /dev/null +++ b/apps/billing-service/Jenkinsfile @@ -0,0 +1,125 @@ +#!/usr/bin/env groovy +pipeline { + agent any + + options { + disableConcurrentBuilds() + } + + environment { + SNAPSHOT_VERSION = readMavenPom().getVersion() + LAST_COMMIT_MESSAGE = "${currentBuild.changeSets.size() == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items[currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length - 1].msg}" + PERFORM_RELEASE = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') && env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ')}" + RELEASE_VERSION = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') ? env.SNAPSHOT_VERSION.substring(0, env.SNAPSHOT_VERSION.lastIndexOf('-SNAPSHOT')) : SNAPSHOT_VERSION}" + VERSION = "${env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ') ? env.RELEASE_VERSION : env.SNAPSHOT_VERSION}" + } + + triggers { + pollSCM("* * * * *") + } + + stages { + stage ('Compile') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + echo "Building version ${env.VERSION}" + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn versions:set -DnewVersion=${env.RELEASE_VERSION} -B" + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.RELEASE_VERSION}/g' deployment/overlays/prod/kustomization.yaml" + } else { + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.GIT_COMMIT}/g' deployment/overlays/test/kustomization.yaml" + } + } + sh 'mvn clean test-compile -B' + } + } + stage ('Test') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh "mvn test -B" + } + } + stage ('Package') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh 'mvn package -Dmaven.test.skip=true -B' + sh 'docker build -t billing .' + } + } + stage ('Push') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh """ + docker tag billing localhost:30010/billing:${env.VERSION} + docker tag billing localhost:30010/billing:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker tag billing localhost:30010/billing:${env.GIT_COMMIT} + docker push localhost:30010/billing:${env.VERSION} + docker push localhost:30010/billing:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker push localhost:30010/billing:${env.GIT_COMMIT} + """ + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh 'git config --global user.name "Jenkins"' + sh 'git config --global user.email "ci@openknowledge.de"' + sh "mvn scm:checkin -Dmessage='release of version ${env.RELEASE_VERSION}' -B" + sh "mvn scm:tag -Dtag=${env.RELEASE_VERSION} -B" + int nextRevision = Integer.parseInt(env.RELEASE_VERSION.substring(env.RELEASE_VERSION.lastIndexOf(".") + 1)) + 1 + nextVersion = RELEASE_VERSION.substring(0, env.RELEASE_VERSION.lastIndexOf(".")) + "." + nextRevision + "-SNAPSHOT" + sh "sed -i 's/${env.RELEASE_VERSION}/${nextVersion}/g' deployment/overlays/prod/kustomization.yaml" + sh "mvn versions:set scm:checkin -DnewVersion=${nextVersion} -Dmessage='update version to ${nextVersion}' -B" + } + } + } + } + stage ('Deploy') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn scm:checkout -DscmVersion=${env.RELEASE_VERSION} -DscmVersionType=tag -B" + sh 'kubectl apply -k deployment/overlays/prod' + } else { + sh 'kubectl apply -k deployment/overlays/test' + sh "sed -i 's/${env.GIT_COMMIT}/${env.SNAPSHOT_VERSION}/g' deployment/overlays/test/kustomization.yaml" + } + } + } + } + } +} diff --git a/apps/billing-service/README.md b/apps/billing-service/README.md new file mode 100644 index 0000000..5e20406 --- /dev/null +++ b/apps/billing-service/README.md @@ -0,0 +1,3 @@ +# cdc-billing-service + +Billing Service to show Consumer-Driven Contracts \ No newline at end of file diff --git a/apps/billing-service/deployment/base/deployment.yaml b/apps/billing-service/deployment/base/deployment.yaml new file mode 100644 index 0000000..3360e51 --- /dev/null +++ b/apps/billing-service/deployment/base/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: billing-deployment + labels: + app: billing-service +spec: + replicas: 1 + selector: + matchLabels: + app: billing-service + template: + metadata: + labels: + app: billing-service + spec: + containers: + - name: billing + image: billing:latest + ports: + - containerPort: 4001 + name: http diff --git a/apps/billing-service/deployment/base/kustomization.yaml b/apps/billing-service/deployment/base/kustomization.yaml new file mode 100644 index 0000000..5b98e94 --- /dev/null +++ b/apps/billing-service/deployment/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml diff --git a/apps/billing-service/deployment/base/service.yaml b/apps/billing-service/deployment/base/service.yaml new file mode 100644 index 0000000..e063702 --- /dev/null +++ b/apps/billing-service/deployment/base/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: billing-service +spec: + selector: + app: billing-service + type: NodePort + ports: + - protocol: TCP + port: 4001 + targetPort: 4001 + nodePort: 30070 + name: service diff --git a/apps/billing-service/deployment/overlays/prod/kustomization.yaml b/apps/billing-service/deployment/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..45820fa --- /dev/null +++ b/apps/billing-service/deployment/overlays/prod/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: prod + +resources: + - ../../base + +patches: + - target: + version: v1 + kind: Service + name: billing-service + path: ./patches/port-patch.yaml + +images: + - name: billing + newName: localhost:30010/billing + newTag: 1.1.0-SNAPSHOT diff --git a/apps/billing-service/deployment/overlays/prod/patches/port-patch.yaml b/apps/billing-service/deployment/overlays/prod/patches/port-patch.yaml new file mode 100644 index 0000000..f5815ed --- /dev/null +++ b/apps/billing-service/deployment/overlays/prod/patches/port-patch.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: billing-service +spec: + selector: + app: billing-service + type: NodePort + ports: + - protocol: TCP + port: 4001 + targetPort: 4001 + nodePort: 31070 + name: service diff --git a/apps/billing-service/deployment/overlays/test/kustomization.yaml b/apps/billing-service/deployment/overlays/test/kustomization.yaml new file mode 100644 index 0000000..4a5e8ae --- /dev/null +++ b/apps/billing-service/deployment/overlays/test/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: test + +resources: + - ../../base + +images: + - name: billing + newName: localhost:30010/billing + newTag: 1.1.0-SNAPSHOT diff --git a/apps/billing-service/pom.xml b/apps/billing-service/pom.xml new file mode 100644 index 0000000..8e780db --- /dev/null +++ b/apps/billing-service/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + de.openkonwledge.sample.shop + billing-service + 1.1.0-SNAPSHOT + Microservices – Service "Billing" + war + + + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/billing-service.git + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/billing-service.git + + + + 11 + 11 + false + UTF-8 + 1.2.13 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.commons + commons-lang3 + 3.9 + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + au.com.dius.pact.provider + junit5 + 4.3.5 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + + + billing-service + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + ${project.version} + + + + + + diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java new file mode 100644 index 0000000..8830a59 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class AddressApplication extends Application { +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java new file mode 100644 index 0000000..70c4799 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.AddressRepository; +import de.openknowledge.sample.address.domain.CustomerNumber; + +/** + * RESTFul endpoint for delivery addresses + */ +@ApplicationScoped +@Path("/billing-addresses") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AddressResource { + + private final static Logger LOGGER = Logger.getLogger(AddressResource.class.getSimpleName()); + + @Inject + private AddressRepository addressesRepository; + + @GET + @Path("/{customerNumber}") + @Produces(MediaType.APPLICATION_JSON) + public Address getAddress(@PathParam("customerNumber") CustomerNumber number) { + LOGGER.info("RESTful call 'GET address'"); + return addressesRepository.find(number).orElseThrow(NotFoundException::new); + } + + @POST + @Path("/{customerNumber}") + @Consumes(MediaType.APPLICATION_JSON) + public Response setAddress(@PathParam("customerNumber") CustomerNumber customerNumber, Address address, + @Context UriInfo uri) { + LOGGER.info("RESTful call 'POST address'"); + addressesRepository.update(customerNumber, address); + return Response.ok().build(); + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 0000000..d7cf6a0 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; + +public class Address { + private Recipient recipient; + private Street street; + private City city; + + @JsonbCreator + public Address(@JsonbProperty("recipient") Recipient recipient) { + this.recipient = notNull(recipient, "recipient may not be null"); + } + + public Address(Recipient recipient, Street street, City city) { + this(recipient); + setStreet(street); + setCity(city); + } + + @JsonbTypeAdapter(Recipient.Adapter.class) + public Recipient getRecipient() { + return recipient; + } + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + @JsonbTypeAdapter(City.Adapter.class) + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java new file mode 100644 index 0000000..f55584e --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.AddressLine.Adapter; + +import static org.apache.commons.lang3.Validate.notNull; + +@JsonbTypeAdapter(Adapter.class) +public class AddressLine { + + public static final AddressLine EMPTY = new AddressLine(""); + + private String line; + + protected AddressLine() { + // for frameworks + } + + public AddressLine(String line) { + this.line = notNull(line, "line may not be null").trim(); + } + + public StreetName getStreetName() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new StreetName(line.substring(0, line.length() - lastSegment.length())); + } else if (containsDigit(firstSegment)) { + return new StreetName(line.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine street name"); + } + } + + public HouseNumber getHouseNumber() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new HouseNumber(lastSegment); + } else if (containsDigit(firstSegment)) { + return new HouseNumber(firstSegment); + } else { + throw new IllegalStateException("Could not determine house number"); + } + } + + @Override + public String toString() { + return line; + } + + + @Override + public int hashCode() { + return line.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AddressLine)) { + return false; + } + + AddressLine recipient = (AddressLine) object; + + return toString().equals(recipient.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public AddressLine adaptFromJson(String name) throws Exception { + return new AddressLine(name); + } + + @Override + public String adaptToJson(AddressLine name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java new file mode 100644 index 0000000..cfa5b33 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.lang.String.format; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +/** + * Addresses repository + */ +@ApplicationScoped +public class AddressRepository { + + private static final Logger LOGGER = Logger.getLogger(AddressRepository.class.getSimpleName()); + + private Map addresses; + + @PostConstruct + public void initialize() { + + addresses = new ConcurrentHashMap<>(); + + addresses.put(new CustomerNumber("0815"), new Address(new Recipient("Max Mustermann"), + new Street(new StreetName("Poststr."), new HouseNumber("1")), new City("26122 Oldenburg"))); + + addresses.put(new CustomerNumber("0816"), new Address(new Recipient("Erika Mustermann"), + new Street(new StreetName("II. Hagen"), new HouseNumber("7")), new City("45127 Essen"))); + LOGGER.info(format("address repository initialized with %d addresses: ", addresses.size())); + } + + public Optional
find(CustomerNumber number) { + return Optional.ofNullable(addresses.get(number)); + } + + public void update(CustomerNumber number, Address address) { + Optional.ofNullable(address).ifPresent(a -> addresses.put(number, a)); + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 0000000..1511048 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public static City valueOf(String name) { + return new City(name); + } + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + protected City() { + // for framework + } + + public ZipCode getZipCode() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new ZipCode(firstSegment); + } else if (containsDigit(lastSegment)) { + return new ZipCode(name.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine zip code"); + } + } + + public CityName getCityName() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new CityName(name.substring(firstSegment.length())); + } else if (containsDigit(lastSegment)) { + return new CityName(name.substring(0, firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine city name"); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City) object; + + return toString().equals(city.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 0000000..f307d92 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName) object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java new file mode 100644 index 0000000..663bc33 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CustomerNumber.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerNumber { + private String number; + + protected CustomerNumber() { + // for frameworks + } + + public CustomerNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof CustomerNumber)) { + return false; + } + + CustomerNumber number = (CustomerNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerNumber adaptFromJson(String number) throws Exception { + return new CustomerNumber(number); + } + + @Override + public String adaptToJson(CustomerNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 0000000..68cbb1e --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + private String number; + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber number = (HouseNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java new file mode 100644 index 0000000..19a2ffa --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Location { + + private ZipCode zipCode; + private CityName cityName; + + @JsonbCreator + public Location(@JsonbProperty("zipCode") ZipCode zipCode, @JsonbProperty("cityName") CityName city) { + this.zipCode = notNull(zipCode, "zip code may not be null"); + this.cityName = notNull(city, "city name may not be null"); + } + + public ZipCode getZipCode() { + return zipCode; + } + + public CityName getCityName() { + return cityName; + } + + @Override + public int hashCode() { + return zipCode.hashCode() ^ cityName.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Location)) { + return false; + } + + Location location = (Location) object; + + return zipCode.equals(location.zipCode) && cityName.equals(location.cityName); + } + + @Override + public String toString() { + return zipCode.isGerman() ? zipCode + " " + cityName : cityName + " " + zipCode; + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java new file mode 100644 index 0000000..e1d3c29 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.Recipient.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class Recipient { + + private String name; + + public static Recipient valueOf(String name) { + return new Recipient(name); + } + + protected Recipient() { + // for frameworks + } + + public Recipient(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + + @Override + public String toString() { + return name; + } + + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Recipient)) { + return false; + } + + Recipient recipient = (Recipient) object; + + return toString().equals(recipient.toString()); + } + + + public static class Adapter implements JsonbAdapter { + + @Override + public Recipient adaptFromJson(String name) throws Exception { + return new Recipient(name); + } + + @Override + public String adaptToJson(Recipient name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 0000000..05c0e9b --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + @JsonbTypeAdapter(StreetName.Adapter.class) + public StreetName getName() { + return name; + } + + @JsonbTypeAdapter(HouseNumber.Adapter.class) + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street) object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 0000000..7d09755 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName)) { + return false; + } + StreetName name = (StreetName) object; + + return toString().equals(name.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 0000000..9709576 --- /dev/null +++ b/apps/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode code = (ZipCode) object; + + return toString().equals(code.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/apps/billing-service/src/test/java/de/openknowledge/sample/address/BillingAddressServiceTest.java b/apps/billing-service/src/test/java/de/openknowledge/sample/address/BillingAddressServiceTest.java new file mode 100644 index 0000000..4a6808e --- /dev/null +++ b/apps/billing-service/src/test/java/de/openknowledge/sample/address/BillingAddressServiceTest.java @@ -0,0 +1,56 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address; + +import org.apache.meecrowave.Meecrowave; +import org.apache.meecrowave.junit5.MonoMeecrowaveConfig; +import org.apache.meecrowave.testing.ConfigurationInject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import au.com.dius.pact.provider.junit5.HttpTestTarget; +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.State; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; + +@Provider("billing-service") +@PactFolder("src/test/pacts") +@MonoMeecrowaveConfig +public class BillingAddressServiceTest { + + @ConfigurationInject + private Meecrowave.Builder config; + + + @BeforeEach + public void setUp(PactVerificationContext context) { + context.setTarget(new HttpTestTarget("localhost", config.getHttpPort(), "/")); + } + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @State("Three customers") + public void toDefaultState() { + System.out.println("Now service in default state"); + } +} diff --git a/apps/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java b/apps/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java new file mode 100644 index 0000000..32f1d9c --- /dev/null +++ b/apps/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java @@ -0,0 +1,41 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address; + +import java.io.InputStream; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; + +import org.assertj.core.api.Condition; + +public class JsonObjectComparision extends Condition> { + + public JsonObjectComparision(JsonObject object) { + super(v -> v.entrySet().containsAll(object.entrySet()), "object containing %s", object); + } + + public static Condition> sameAs(InputStream in) { + return new JsonObjectComparision(Json.createReader(in).readObject()); + } + + public static Condition thatIsSameAs(InputStream in) { + Condition condition = new JsonObjectComparision(Json.createReader(in).readObject()); + return (Condition)condition; + } +} diff --git a/apps/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java b/apps/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java new file mode 100644 index 0000000..ed5ef08 --- /dev/null +++ b/apps/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Specializes; + +@Specializes +@RequestScoped +public class TestAddressRepository extends AddressRepository { + +} diff --git a/apps/billing-service/src/test/pacts/customer-service-billing-service.json b/apps/billing-service/src/test/pacts/customer-service-billing-service.json new file mode 100644 index 0000000..22b29ef --- /dev/null +++ b/apps/billing-service/src/test/pacts/customer-service-billing-service.json @@ -0,0 +1,111 @@ +{ + "provider": { + "name": "billing-service" + }, + "consumer": { + "name": "customer-service" + }, + "interactions": [ + { + "description": "GET request for 0815", + "request": { + "method": "GET", + "path": "/billing-addresses/0815" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json; charset=UTF-8" + }, + "body": { + "city": "26122 Oldenburg", + "street": { + "number": "1", + "name": "Poststr." + }, + "recipient": "Max Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json(;\\s?charset=[\\w\\-]+)?" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "GET request for 0817", + "request": { + "method": "GET", + "path": "/billing-addresses/0817" + }, + "response": { + "status": 404 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for 0815", + "request": { + "method": "POST", + "path": "/billing-addresses/0815", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "45127 Essen", + "street": { + "number": "7", + "name": "II. Hagen" + }, + "recipient": "Erika Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + } + ], + "metadata": { + "pactSpecification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "4.1.7" + } + } +} diff --git a/apps/billing-service/src/test/resources/de/openknowledge/sample/address/007.json b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/007.json new file mode 100644 index 0000000..481cc89 --- /dev/null +++ b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/007.json @@ -0,0 +1,8 @@ +{ + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" +} diff --git a/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json new file mode 100644 index 0000000..9541839 --- /dev/null +++ b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json new file mode 100644 index 0000000..84bb8d0 --- /dev/null +++ b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json @@ -0,0 +1,8 @@ +{ + "recipient": "Max Mustermann", + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 Oldenburg" +} diff --git a/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json new file mode 100644 index 0000000..9541839 --- /dev/null +++ b/apps/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/apps/customer-service/.gitignore b/apps/customer-service/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/apps/customer-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/apps/customer-service/Dockerfile b/apps/customer-service/Dockerfile new file mode 100644 index 0000000..e1012b4 --- /dev/null +++ b/apps/customer-service/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:11-jre + +RUN wget https://repo.maven.apache.org/maven2/org/apache/meecrowave/meecrowave-core/1.2.13/meecrowave-core-1.2.13-runner.jar -O /opt/meecrowave-core-runner.jar +ADD target/customer-service.war /opt/customer-service.war + +EXPOSE 4000 +ENTRYPOINT ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "/opt/meecrowave-core-runner.jar", "--http", "4000", "--webapp", "/opt/customer-service.war"] diff --git a/apps/customer-service/Jenkinsfile b/apps/customer-service/Jenkinsfile new file mode 100644 index 0000000..f662ba0 --- /dev/null +++ b/apps/customer-service/Jenkinsfile @@ -0,0 +1,125 @@ +#!/usr/bin/env groovy +pipeline { + agent any + + options { + disableConcurrentBuilds() + } + + environment { + SNAPSHOT_VERSION = readMavenPom().getVersion() + LAST_COMMIT_MESSAGE = "${currentBuild.changeSets.size() == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items[currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length - 1].msg}" + PERFORM_RELEASE = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') && env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ')}" + RELEASE_VERSION = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') ? env.SNAPSHOT_VERSION.substring(0, env.SNAPSHOT_VERSION.lastIndexOf('-SNAPSHOT')) : SNAPSHOT_VERSION}" + VERSION = "${env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ') ? env.RELEASE_VERSION : env.SNAPSHOT_VERSION}" + } + + triggers { + pollSCM("* * * * *") + } + + stages { + stage ('Compile') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + echo "Building version ${env.VERSION}" + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn versions:set -DnewVersion=${env.RELEASE_VERSION} -B" + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.RELEASE_VERSION}/g' deployment/overlays/prod/kustomization.yaml" + } else { + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.GIT_COMMIT}/g' deployment/overlays/test/kustomization.yaml" + } + } + sh 'mvn clean test-compile -B' + } + } + stage ('Test') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh "mvn test -B" + } + } + stage ('Package') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh 'mvn package -DskipTests -B' + sh 'docker build -t customer .' + } + } + stage ('Push') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh """ + docker tag customer localhost:30010/customer:${env.VERSION} + docker tag customer localhost:30010/customer:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker tag customer localhost:30010/customer:${env.GIT_COMMIT} + docker push localhost:30010/customer:${env.VERSION} + docker push localhost:30010/customer:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker push localhost:30010/customer:${env.GIT_COMMIT} + """ + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh 'git config --global user.name "Jenkins"' + sh 'git config --global user.email "ci@openknowledge.de"' + sh "mvn scm:checkin -Dmessage='release of version ${env.RELEASE_VERSION}' -B" + sh "mvn scm:tag -Dtag=${env.RELEASE_VERSION} -B" + int nextRevision = Integer.parseInt(env.RELEASE_VERSION.substring(env.RELEASE_VERSION.lastIndexOf(".") + 1)) + 1 + nextVersion = RELEASE_VERSION.substring(0, env.RELEASE_VERSION.lastIndexOf(".")) + "." + nextRevision + "-SNAPSHOT" + sh "sed -i 's/${env.RELEASE_VERSION}/${nextVersion}/g' deployment/overlays/prod/kustomization.yaml" + sh "mvn versions:set scm:checkin -DnewVersion=${nextVersion} -Dmessage='update version to ${nextVersion}' -B" + } + } + } + } + stage ('Deploy') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn scm:checkout -DscmVersion=${env.RELEASE_VERSION} -DscmVersionType=tag -B" + sh 'kubectl apply -k deployment/overlays/prod' + } else { + sh 'kubectl apply -k deployment/overlays/test' + sh "sed -i 's/${env.GIT_COMMIT}/${env.SNAPSHOT_VERSION}/g' deployment/overlays/test/kustomization.yaml" + } + } + } + } + } +} diff --git a/apps/customer-service/README.md b/apps/customer-service/README.md new file mode 100644 index 0000000..d2f796d --- /dev/null +++ b/apps/customer-service/README.md @@ -0,0 +1,3 @@ +# cdc-customer-service + +Customer Service to show Consumer-Driven Contracts \ No newline at end of file diff --git a/apps/customer-service/deployment/base/deployment.yaml b/apps/customer-service/deployment/base/deployment.yaml new file mode 100644 index 0000000..8749c92 --- /dev/null +++ b/apps/customer-service/deployment/base/deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: customer-deployment + labels: + app: customer-service +spec: + replicas: 1 + selector: + matchLabels: + app: customer-service + template: + metadata: + labels: + app: customer-service + spec: + containers: + - name: customer + image: customer:latest + ports: + - containerPort: 4000 + name: http + env: + - name: BILLING_SERVICE_URL + value: http://billing-service:4001 + - name: DELIVERY_SERVICE_URL + value: http://delivery-service:4002 diff --git a/apps/customer-service/deployment/base/kustomization.yaml b/apps/customer-service/deployment/base/kustomization.yaml new file mode 100644 index 0000000..5b98e94 --- /dev/null +++ b/apps/customer-service/deployment/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml diff --git a/apps/customer-service/deployment/base/service.yaml b/apps/customer-service/deployment/base/service.yaml new file mode 100644 index 0000000..c73d775 --- /dev/null +++ b/apps/customer-service/deployment/base/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: customer-service +spec: + selector: + app: customer-service + type: NodePort + ports: + - protocol: TCP + port: 4000 + targetPort: 4000 + nodePort: 30060 + name: service diff --git a/apps/customer-service/deployment/overlays/prod/kustomization.yaml b/apps/customer-service/deployment/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..dae906f --- /dev/null +++ b/apps/customer-service/deployment/overlays/prod/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: prod + +resources: + - ../../base + +patches: + - target: + version: v1 + kind: Service + name: customer-service + path: ./patches/port-patch.yaml + +images: + - name: customer + newName: localhost:30010/customer + newTag: 1.1.0-SNAPSHOT diff --git a/apps/customer-service/deployment/overlays/prod/patches/port-patch.yaml b/apps/customer-service/deployment/overlays/prod/patches/port-patch.yaml new file mode 100644 index 0000000..cebc05e --- /dev/null +++ b/apps/customer-service/deployment/overlays/prod/patches/port-patch.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: customer-service +spec: + selector: + app: customer-service + type: NodePort + ports: + - protocol: TCP + port: 4000 + targetPort: 4000 + nodePort: 31060 + name: service diff --git a/apps/customer-service/deployment/overlays/test/kustomization.yaml b/apps/customer-service/deployment/overlays/test/kustomization.yaml new file mode 100644 index 0000000..5d6870e --- /dev/null +++ b/apps/customer-service/deployment/overlays/test/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: test + +resources: + - ../../base + +images: + - name: customer + newName: localhost:30010/customer + newTag: 1.1.0-SNAPSHOT diff --git a/apps/customer-service/pom.xml b/apps/customer-service/pom.xml new file mode 100644 index 0000000..676a487 --- /dev/null +++ b/apps/customer-service/pom.xml @@ -0,0 +1,133 @@ + + + + + 4.0.0 + + de.openkonwledge.sample.shop + customer-service + 1.1.0-SNAPSHOT + Microservices – Service "Customers" + war + + + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/customer-service.git + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/customer-service.git + + + + 11 + 11 + false + UTF-8 + 1.2.13 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.meecrowave + meecrowave-core + ${meecrowave.version} + provided + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + org.eclipse.microprofile.config + microprofile-config-api + 1.3 + + + org.apache.commons + commons-lang3 + 3.9 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + au.com.dius.pact.consumer + junit5 + 4.3.5 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + rocks.limburg.cdimock + cdimock + 1.0.4 + test + + + org.apache.geronimo.config + geronimo-config-impl + 1.2.2 + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + + + customer-service + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + ${project.version} + + + + + + diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 0000000..4a012bd --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.Validate.notNull; + +import java.util.Objects; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; + +public class Address { + private Recipient recipient; + private Street street; + private City city; + + @JsonbCreator + public Address(@JsonbProperty("recipient") Recipient recipient) { + this.recipient = notNull(recipient, "recipient may not be null"); + } + + public Address(Recipient recipient, Street street, City city) { + this(recipient); + setStreet(street); + setCity(city); + } + + public Recipient getRecipient() { + return recipient; + } + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } + + @Override + public int hashCode() { + return recipient.hashCode() + ofNullable(street).map(Object::hashCode).orElse(0) + ofNullable(city).map(Object::hashCode).orElse(0); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || !getClass().equals(object.getClass())) { + return false; + } + Address address = (Address)object; + return recipient.equals(recipient) && Objects.equals(street, address.street) && Objects.equals(city, address.city); + } + + public static Builder of(String recipient) { + return new Builder(new Recipient(recipient)); + } + + public static class Builder { + + private Address address; + + private Builder(Recipient recipient) { + address = new Address(recipient); + } + + public Builder atStreet(String name) { + AddressLine addressLine = new AddressLine(name); + address.setStreet(new Street(addressLine.getStreetName(), addressLine.getHouseNumber())); + return this; + } + + public Builder inCity(String city) { + address.setCity(new City(city)); + return this; + } + + public Address build() { + return address; + } + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java new file mode 100644 index 0000000..f55584e --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.AddressLine.Adapter; + +import static org.apache.commons.lang3.Validate.notNull; + +@JsonbTypeAdapter(Adapter.class) +public class AddressLine { + + public static final AddressLine EMPTY = new AddressLine(""); + + private String line; + + protected AddressLine() { + // for frameworks + } + + public AddressLine(String line) { + this.line = notNull(line, "line may not be null").trim(); + } + + public StreetName getStreetName() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new StreetName(line.substring(0, line.length() - lastSegment.length())); + } else if (containsDigit(firstSegment)) { + return new StreetName(line.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine street name"); + } + } + + public HouseNumber getHouseNumber() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new HouseNumber(lastSegment); + } else if (containsDigit(firstSegment)) { + return new HouseNumber(firstSegment); + } else { + throw new IllegalStateException("Could not determine house number"); + } + } + + @Override + public String toString() { + return line; + } + + + @Override + public int hashCode() { + return line.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AddressLine)) { + return false; + } + + AddressLine recipient = (AddressLine) object; + + return toString().equals(recipient.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public AddressLine adaptFromJson(String name) throws Exception { + return new AddressLine(name); + } + + @Override + public String adaptToJson(AddressLine name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java new file mode 100644 index 0000000..be1542d --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static javax.ws.rs.client.Entity.entity; +import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; + +import java.util.Optional; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import de.openknowledge.sample.customer.domain.CustomerNumber; + +@ApplicationScoped +public class BillingAddressRepository { + + private static final Logger LOG = Logger.getLogger(BillingAddressRepository.class.getSimpleName()); + private static final String BILLING_ADDRESSES_PATH = "billing-addresses"; + + @Inject + @ConfigProperty(name = "billing-service.url") + String billingServiceUrl; + + Client client; + + @PostConstruct + public void newClient() { + client = ClientBuilder.newClient(); + } + + public Optional
find(CustomerNumber customerNumber) { + LOG.info("load billing address from " + billingServiceUrl); + return Optional.of(client + .register(JsonbJaxrsProvider.class) + .target(billingServiceUrl) + .path(BILLING_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .get()) + .filter(r -> r.getStatusInfo().getFamily() == SUCCESSFUL) + .filter(Response::hasEntity) + .map(r -> r.readEntity(Address.class)); + } + + public void update(CustomerNumber customerNumber, Address billingAddress) { + LOG.info("update billing address at " + billingServiceUrl); + client.target(billingServiceUrl) + .path(BILLING_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .post(entity(billingAddress, MediaType.APPLICATION_JSON_TYPE)); + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 0000000..100abb7 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + protected City() { + // for framework + } + + public ZipCode getZipCode() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new ZipCode(firstSegment); + } else if (containsDigit(lastSegment)) { + return new ZipCode(name.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine zip code"); + } + } + + public CityName getCityName() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new CityName(name.substring(firstSegment.length())); + } else if (containsDigit(lastSegment)) { + return new CityName(name.substring(0, firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine city name"); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City) object; + + return toString().equals(city.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 0000000..f307d92 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName) object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java new file mode 100644 index 0000000..061d389 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static javax.ws.rs.client.Entity.entity; +import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; + +import java.io.StringReader; +import java.util.Optional; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.validation.ValidationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import de.openknowledge.sample.customer.domain.CustomerNumber; + +@ApplicationScoped +public class DeliveryAddressRepository { + + private static final Logger LOG = Logger.getLogger(DeliveryAddressRepository.class.getSimpleName()); + private static final String DELIVERY_ADDRESSES_PATH = "delivery-addresses"; + + @Inject + @ConfigProperty(name = "delivery-service.url") + String deliveryServiceUrl; + + Client client; + + @PostConstruct + public void newClient() { + client = ClientBuilder.newClient(); + } + + public Optional
find(CustomerNumber customerNumber) { + LOG.info("load delivery address from " + deliveryServiceUrl); + return Optional.of(client + .target(deliveryServiceUrl) + .path(DELIVERY_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .get()) + .filter(r -> r.getStatusInfo().getFamily() == SUCCESSFUL) + .filter(Response::hasEntity) + .map(r -> r.readEntity(Address.class)); + } + + public void update(CustomerNumber customerNumber, Address deliveryAddress) { + LOG.info("update delivery address at " + deliveryServiceUrl); + Response response = client + .target(deliveryServiceUrl) + .path(DELIVERY_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .post(entity(deliveryAddress, MediaType.APPLICATION_JSON_TYPE)); + handleValidationError(response); + } + + private void handleValidationError(Response response) { + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + return; + } + if (!response.hasEntity()) { + throw new ValidationException("invalid address"); + } + JsonObject problem = Json.createReader(new StringReader(response.readEntity(String.class))).readObject(); + throw new ValidationException(problem.getString("detail")); + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 0000000..0170f61 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + private String number; + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber number = (HouseNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java new file mode 100644 index 0000000..13fe126 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.Recipient.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class Recipient { + + private String name; + + public static Recipient valueOf(String name) { + return new Recipient(name); + } + + protected Recipient() { + // for frameworks + } + + public Recipient(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + + @Override + public String toString() { + return name; + } + + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Recipient)) { + return false; + } + + Recipient recipient = (Recipient) object; + + return toString().equals(recipient.toString()); + } + + + public static class Adapter implements JsonbAdapter { + + @Override + public Recipient adaptFromJson(String name) throws Exception { + return new Recipient(name); + } + + @Override + public String adaptToJson(Recipient name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 0000000..5244426 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street) object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 0000000..7d09755 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName)) { + return false; + } + StreetName name = (StreetName) object; + + return toString().equals(name.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 0000000..9709576 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode code = (ZipCode) object; + + return toString().equals(code.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java new file mode 100644 index 0000000..fbfce55 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CORSFilter implements ContainerResponseFilter { + + @Override + public void filter(final ContainerRequestContext requestContext, + final ContainerResponseContext cres) throws IOException { + cres.getHeaders().add("Access-Control-Allow-Origin", "*"); + cres.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + cres.getHeaders().add("Access-Control-Allow-Credentials", "true"); + cres.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + cres.getHeaders().add("Access-Control-Max-Age", "1209600"); + } + +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java new file mode 100644 index 0000000..dcc1fd1 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -0,0 +1,32 @@ +package de.openknowledge.sample.address.infrastructure; + +import javax.validation.ValidationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class ValidationExceptionHandler implements ExceptionMapper { + + private static final String PROBLEM_JSON_TYPE = "application/problem+json"; + private static final String PROBLEM_JSON + = "{\"type\": \"%s\", \"title\": \"%s\", \"status\": %d, \"detail\": \"%s\", \"instance\": \"%s\"}"; + + @Context + private UriInfo uri; + + @Override + public Response toResponse(ValidationException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .type(PROBLEM_JSON_TYPE) + .entity(String.format( + PROBLEM_JSON, + uri.getBaseUri().resolve("/errors/invalid-" + uri.getPathSegments().get(uri.getPathSegments().size() - 1)), + "bad request", Response.Status.BAD_REQUEST.getStatusCode(), + exception.getMessage(), + uri.getAbsolutePath().toString())) + .build(); + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java new file mode 100644 index 0000000..176e559 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class CustomerApplication extends Application { +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java new file mode 100644 index 0000000..9aed674 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.application; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.BillingAddressRepository; +import de.openknowledge.sample.address.domain.DeliveryAddressRepository; +import de.openknowledge.sample.customer.domain.Customer; +import de.openknowledge.sample.customer.domain.CustomerNumber; +import de.openknowledge.sample.customer.domain.CustomerRepository; + +/** + * RESTFul endpoint for customers + */ +@ApplicationScoped +@Path("/customers") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class CustomerResource { + + private final static Logger LOG = Logger.getLogger(CustomerResource.class.getSimpleName()); + + @Inject + private CustomerRepository customerRepository; + @Inject + private BillingAddressRepository billingAddressRepository; + @Inject + private DeliveryAddressRepository deliveryAddressRepository; + + @GET + @Path("/") + @Produces(MediaType.APPLICATION_JSON) + public List getCustomers() { + LOG.info("RESTful call 'GET all customers'"); + return customerRepository.findAll(); + } + + @POST + @Path("/") + @Consumes(MediaType.APPLICATION_JSON) + public Response createCustomer(Customer customer, @Context UriInfo uri) throws URISyntaxException { + LOG.info("RESTful call 'POST new customer'"); + customerRepository.persist(customer); + return Response.created(uri.getAbsolutePathBuilder().path(customer.getNumber().toString()).build()).build(); + } + + @GET + @Path("/{customerNumber}") + @Produces(MediaType.APPLICATION_JSON) + public Customer getCustomer(@PathParam("customerNumber") CustomerNumber customerNumber) { + LOG.info("RESTful call 'GET customer'"); + Customer customer = customerRepository.find(customerNumber).orElseThrow(customerNotFound(customerNumber)); + billingAddressRepository.find(customerNumber).ifPresent(customer::setBillingAddress); + deliveryAddressRepository.find(customerNumber).ifPresent(customer::setDeliveryAddress); + return customer; + } + + @PUT + @Path("/{customerNumber}/billing-address") + @Produces(MediaType.APPLICATION_JSON) + public void setBillingAddress(@PathParam("customerNumber") CustomerNumber customerNumber, Address billingAddress) { + LOG.info("RESTful call 'PUT billing address'"); + customerRepository.find(customerNumber).orElseThrow(customerNotFound(customerNumber)); + billingAddressRepository.update(customerNumber, billingAddress); + } + + @PUT + @Path("/{customerNumber}/delivery-address") + @Produces(MediaType.APPLICATION_JSON) + public void setDeliveryAddress(@PathParam("customerNumber") CustomerNumber customerNumber, + Address deliveryAddress) { + LOG.info("RESTful call 'PUT delivery address'"); + customerRepository.find(customerNumber).orElseThrow(customerNotFound(customerNumber)); + deliveryAddressRepository.update(customerNumber, deliveryAddress); + } + + private Supplier customerNotFound(CustomerNumber number) { + return () -> new NotFoundException("customer " + number + " not found"); + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java new file mode 100644 index 0000000..bd8059e --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.Address; + +public class Customer { + + CustomerNumber number; + private CustomerName name; + private Address billingAddress; + private Address deliveryAddress; + + @JsonbCreator + public Customer(@JsonbProperty("name") CustomerName name) { + this.name = notNull(name, "name may not be null"); + } + + public Customer(CustomerNumber number, CustomerName name) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(number, "number may not be null"); + } + + public CustomerName getName() { + return name; + } + + public CustomerNumber getNumber() { + return number; + } + + @JsonbProperty(nillable = false) + public Address getBillingAddress() { + return billingAddress; + } + + public void setBillingAddress(Address billingAddress) { + this.billingAddress = billingAddress; + } + + @JsonbProperty(nillable = false) + public Address getDeliveryAddress() { + return deliveryAddress; + } + + public void setDeliveryAddress(Address deliveryAddress) { + this.deliveryAddress = deliveryAddress; + } + + Customer clearAddresses() { + deliveryAddress = null; + billingAddress = null; + return this; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Customer)) { + return false; + } + + Customer customer = (Customer) object; + + return name.equals(customer.getName()) && number.equals(customer.getNumber()); + } + + @Override + public String toString() { + return name + "(" + number + ")"; + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java new file mode 100644 index 0000000..804be90 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.customer.domain.CustomerName.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerName { + + private String name; + + public static CustomerName valueOf(String value) { + return new CustomerName(value); + } + + protected CustomerName() { + // for frameworks + } + + public CustomerName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof CustomerName)) { + return false; + } + CustomerName name = (CustomerName) object; + + return toString().equals(name.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerName adaptFromJson(String name) throws Exception { + return new CustomerName(name); + } + + @Override + public String adaptToJson(CustomerName name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java new file mode 100644 index 0000000..3853899 --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import de.openknowledge.sample.customer.domain.CustomerNumber.Adapter; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerNumber { + private String number; + + protected CustomerNumber() { + // for frameworks + } + + public CustomerNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof CustomerNumber)) { + return false; + } + + CustomerNumber number = (CustomerNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerNumber adaptFromJson(String number) throws Exception { + return new CustomerNumber(number); + } + + @Override + public String adaptToJson(CustomerNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java new file mode 100644 index 0000000..ca17dfd --- /dev/null +++ b/apps/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +/** + * Addresses repository + */ +@ApplicationScoped +public class CustomerRepository { + + private static final Logger LOGGER = Logger.getLogger(CustomerRepository.class.getSimpleName()); + private static final AtomicInteger CUSTOMER_NUMBERS = new AtomicInteger(0); + + private Map customers; + + @PostConstruct + public void initialize() { + + customers = new ConcurrentHashMap<>(); + customers.put(new CustomerNumber("0815"), + new Customer(new CustomerNumber("0815"), new CustomerName("Max Mustermann"))); + customers.put(new CustomerNumber("0816"), + new Customer(new CustomerNumber("0816"), new CustomerName("Erika Mustermann"))); + customers.put(new CustomerNumber("007"), + new Customer(new CustomerNumber("007"), new CustomerName("James Bond"))); + LOGGER.info(format("customer repository initialized with %d customers: ", customers.size())); + } + + public List findAll() { + return customers.values().stream().map(Customer::clearAddresses).collect(toList()); + } + + public void persist(Customer customer) { + customer.number = new CustomerNumber(Integer.toString(CUSTOMER_NUMBERS.incrementAndGet())); + customers.put(customer.number, customer); + } + + public Optional find(CustomerNumber customerNumber) { + return Optional.ofNullable(customers.get(customerNumber)); + } +} diff --git a/apps/customer-service/src/main/resources/META-INF/microprofile-config.properties b/apps/customer-service/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 0000000..ba06d32 --- /dev/null +++ b/apps/customer-service/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,2 @@ +billing-service.url=http://localhost:4001 +delivery-service.url=http://localhost:4002 diff --git a/apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/BillingAddressRepositoryTest.java b/apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/BillingAddressRepositoryTest.java new file mode 100644 index 0000000..9eb437a --- /dev/null +++ b/apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/BillingAddressRepositoryTest.java @@ -0,0 +1,130 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.Optional; + +import javax.ws.rs.client.ClientBuilder; + +import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; +import au.com.dius.pact.consumer.junit5.PactTestFor; +import au.com.dius.pact.core.model.PactSpecVersion; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.annotations.Pact; +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; + +import au.com.dius.pact.consumer.dsl.PactDslJsonBody; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import de.openknowledge.sample.customer.domain.CustomerNumber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(PactConsumerTestExt.class) +@PactTestFor(providerName = "billing-service", pactVersion = PactSpecVersion.V3) +public class BillingAddressRepositoryTest { + + private BillingAddressRepository repository; + + @Pact(consumer = "customer-service") + public RequestResponsePact getMax(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("GET request for 0815") + .path("/billing-addresses/0815") + .method("GET") + .willRespondWith() + .status(200) + .body(new PactDslJsonBody() + .stringValue("recipient", "Max Mustermann") + .stringValue("city", "26122 Oldenburg") + .object("street") + .stringValue("name", "Poststr.") + .stringValue("number", "1") + .closeObject()) + .toPact(); + } + + @Pact(consumer = "customer-service") + public RequestResponsePact dontGetMissing(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("GET request for 0817") + .path("/billing-addresses/0817") + .method("GET") + .willRespondWith() + .status(404) + .toPact(); + } + + @Pact(consumer = "customer-service") + public RequestResponsePact updateMax(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("POST request for 0815") + .path("/billing-addresses/0815") + .method("POST") + .matchHeader("Content-Type", "application/json.*", "application/json") + .body(new PactDslJsonBody() + .stringValue("recipient", "Erika Mustermann") + .stringValue("city", "45127 Essen") + .object("street") + .stringValue("name", "II. Hagen") + .stringValue("number", "7") + .closeObject()) + .willRespondWith() + .status(200) + .toPact(); + } + + @BeforeEach + public void initializeRepository(MockServer mockServer) { + repository = new BillingAddressRepository(); + repository.billingServiceUrl = "http://localhost:" + mockServer.getPort(); + repository.client = ClientBuilder.newClient().register(JsonbJaxrsProvider.class); + } + + @PactTestFor(pactMethod = "getMax") + @Test + public void findDeliveryAddressForExistingCustomer() { + Optional
address = repository.find(new CustomerNumber("0815")); + assertThat(address).isPresent().contains( + new Address( + new Recipient("Max Mustermann"), + new Street(new StreetName("Poststr."), new HouseNumber("1")), + new City("26122 Oldenburg"))); + } + + @PactTestFor(pactMethod = "dontGetMissing") + @Test + public void dontFindNonExistingAddress() { + Optional
address = repository.find(new CustomerNumber("0817")); + assertThat(address).isNotPresent(); + } + + @PactTestFor(pactMethod = "updateMax") + @Test + public void updateAddress() { + repository.update(new CustomerNumber("0815"), new Address( + new Recipient("Erika Mustermann"), + new Street(new StreetName("II. Hagen"), new HouseNumber("7")), + new City("45127 Essen"))); + } +} diff --git a/apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/DeliveryAddressRepositoryTest.java b/apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/DeliveryAddressRepositoryTest.java new file mode 100644 index 0000000..ca8fe35 --- /dev/null +++ b/apps/customer-service/src/test/java/de/openknowledge/sample/address/domain/DeliveryAddressRepositoryTest.java @@ -0,0 +1,166 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.util.Optional; + +import javax.validation.ValidationException; +import javax.ws.rs.client.ClientBuilder; + +import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; +import au.com.dius.pact.consumer.junit5.PactTestFor; +import au.com.dius.pact.core.model.PactSpecVersion; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.annotations.Pact; +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; + +import au.com.dius.pact.consumer.dsl.PactDslJsonBody; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import de.openknowledge.sample.customer.domain.CustomerNumber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(PactConsumerTestExt.class) +@PactTestFor(providerName = "delivery-service", pactVersion = PactSpecVersion.V3) +public class DeliveryAddressRepositoryTest { + + private DeliveryAddressRepository repository; + + @Pact(consumer = "customer-service") + public RequestResponsePact getMax(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("GET request for 0815") + .path("/delivery-addresses/0815") + .method("GET") + .willRespondWith() + .status(200) + .body(new PactDslJsonBody() + .stringValue("recipient", "Max Mustermann") + .stringValue("city", "26122 Oldenburg") + .object("street") + .stringValue("name", "Poststr.") + .stringValue("number", "1") + .closeObject()) + .toPact(); + } + + @Pact(consumer = "customer-service") + public RequestResponsePact dontGetMissing(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("GET request for 0817") + .path("/delivery-addresses/0817") + .method("GET") + .willRespondWith() + .status(404) + .toPact(); + } + + @Pact(consumer = "customer-service") + public RequestResponsePact updateMax(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("POST request for 0815") + .path("/delivery-addresses/0815") + .method("POST") + .matchHeader("Content-Type", "application/json.*", "application/json") + .body(new PactDslJsonBody() + .stringValue("recipient", "Erika Mustermann") + .stringValue("city", "45127 Essen") + .object("street") + .stringValue("name", "II. Hagen") + .stringValue("number", "7") + .closeObject()) + .willRespondWith() + .status(200) + .toPact(); + } + + @Pact(consumer = "customer-service") + public RequestResponsePact dontUpdateSherlock(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("POST request for 007") + .path("/delivery-addresses/007") + .method("POST") + .matchHeader("Content-Type", "application/json.*", "application/json") + .body(new PactDslJsonBody() + .stringValue("recipient", "Sherlock Holmes") + .stringValue("city", "London NW1 6XE") + .object("street") + .stringValue("name", "Baker Street") + .stringValue("number", "221B") + .closeObject()) + .willRespondWith() + .status(400) + .matchHeader("Content-Type", "application/problem\\+json.*", "application/problem+json") + .body(new PactDslJsonBody().stringMatcher("detail", ".*", "Addresses from UK are not supported for delivery")) + .toPact(); + } + + @BeforeEach + public void initializeRepository(MockServer mockServer) { + repository = new DeliveryAddressRepository(); + repository.deliveryServiceUrl = "http://localhost:" + mockServer.getPort(); + repository.client = ClientBuilder.newClient().register(JsonbJaxrsProvider.class); + } + + @PactTestFor(pactMethod = "getMax") + @Test + public void findDeliveryAddressForExistingCustomer() { + Optional
address = repository.find(new CustomerNumber("0815")); + assertThat(address).isPresent().contains( + new Address( + new Recipient("Max Mustermann"), + new Street(new StreetName("Poststr."), new HouseNumber("1")), + new City("26122 Oldenburg"))); + } + + @PactTestFor(pactMethod = "dontGetMissing") + @Test + public void dontFindNonExistingAddress() { + Optional
address = repository.find(new CustomerNumber("0817")); + assertThat(address).isNotPresent(); + } + + @PactTestFor(pactMethod = "updateMax") + @Test + public void updateAddress() { + repository.update(new CustomerNumber("0815"), new Address( + new Recipient("Erika Mustermann"), + new Street(new StreetName("II. Hagen"), new HouseNumber("7")), + new City("45127 Essen"))); + } + + @PactTestFor(pactMethod = "dontUpdateSherlock") + @Test + public void dontUpdateInvalidAddress() { + assertThatThrownBy(() -> + repository.update(new CustomerNumber("007"), new Address( + new Recipient("Sherlock Holmes"), + new Street(new StreetName("Baker Street"), new HouseNumber("221B")), + new City("London NW1 6XE")))) + .isInstanceOf(ValidationException.class) + .hasMessage("Addresses from UK are not supported for delivery"); + } +} diff --git a/apps/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java b/apps/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java new file mode 100644 index 0000000..e7ffec1 --- /dev/null +++ b/apps/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java @@ -0,0 +1,210 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.openknowledge.sample.customer; + +import static de.openknowledge.sample.customer.JsonObjectComparision.sameAs; +import static de.openknowledge.sample.customer.JsonObjectComparision.thatIsSameAs; +import static javax.ws.rs.client.Entity.entity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.StringReader; +import java.net.URI; +import java.util.Optional; + +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.meecrowave.Meecrowave; +import org.apache.meecrowave.junit5.MeecrowaveConfig; +import org.apache.meecrowave.testing.ConfigurationInject; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.BillingAddressRepository; +import de.openknowledge.sample.address.domain.DeliveryAddressRepository; +import de.openknowledge.sample.customer.domain.CustomerNumber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import rocks.limburg.cdimock.MockitoBeans; + +@MockitoBeans(types = {BillingAddressRepository.class, DeliveryAddressRepository.class}) +@MeecrowaveConfig +public class CustomerServiceTest { + + @ConfigurationInject + private Meecrowave.Builder config; + + @Inject + private BillingAddressRepository billingAddressRepository; + + @Inject + DeliveryAddressRepository deliveryAddressRepository; + + private URI uri; + + @BeforeEach + public void setUp() { + when(deliveryAddressRepository.find(new CustomerNumber("0815"))) + .thenReturn(Optional.of(Address.of("Max Mustermann").atStreet("Poststrasse 1").inCity("26122 Oldenburg").build())); + when(deliveryAddressRepository.find(new CustomerNumber("0816"))) + .thenReturn(Optional.of(Address.of("Erika Mustermann").atStreet("II. Hagen 7").inCity("45127 Essen").build())); + + when(billingAddressRepository.find(new CustomerNumber("0815"))) + .thenReturn(Optional.of(Address.of("Max Mustermann").atStreet("Poststrasse 1").inCity("26122 Oldenburg").build())); + when(billingAddressRepository.find(new CustomerNumber("007"))) + .thenReturn(Optional.of(Address.of("Sherlock Holmes").atStreet("221B Baker Street").inCity("London NW1 6XE").build())); + + uri = URI.create("http://localhost:" + config.getHttpPort()); + } + + @Test + public void getCustomers() { + JsonArray result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readArray(); + assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("max.json"))); + assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("erika.json"))); + assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("james.json"))); + } + + @Test + public void getCustomerWithAddresses() { + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .path("0815") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("max-with-addresses.json"))); + } + + @Test + public void createCustomer() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers") + .request(MediaType.APPLICATION_JSON) + .post(entity(getClass().getResourceAsStream("sherlock.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.CREATED.getStatusCode()); + + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(response.getLocation()) + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("sherlock.json"))); + + JsonArray customers = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readArray(); + assertThat(customers).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("sherlock.json"))); + } + + @Test + public void getCustomerWithBillingAddress() { + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .path("007") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("james-with-addresses.json"))); + } + + @Test + public void getCustomerWithDeliveryAddress() { + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .path("0816") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("erika-with-addresses.json"))); + } + + @Test + public void setBillingAddressOfExistingCustomer() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0816/billing-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + + @Test + public void setBillingAddressOfNonExistingCustomerFails() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0817/billing-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); + } + + + @Test + public void setDeliveryAddressOfExistingCustomer() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0816/delivery-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + + @Test + public void setDeliveryAddressOfNonExistingCustomerFails() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0817/delivery-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); + } +} diff --git a/apps/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java b/apps/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java new file mode 100644 index 0000000..3bcefea --- /dev/null +++ b/apps/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java @@ -0,0 +1,41 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer; + +import java.io.InputStream; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; + +import org.assertj.core.api.Condition; + +public class JsonObjectComparision extends Condition> { + + public JsonObjectComparision(JsonObject object) { + super(v -> v.entrySet().containsAll(object.entrySet()), "object containing %s", object); + } + + public static Condition> sameAs(InputStream in) { + return new JsonObjectComparision(Json.createReader(in).readObject()); + } + + public static Condition thatIsSameAs(InputStream in) { + Condition condition = new JsonObjectComparision(Json.createReader(in).readObject()); + return (Condition)condition; + } +} diff --git a/apps/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java b/apps/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java new file mode 100644 index 0000000..fdc16d0 --- /dev/null +++ b/apps/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java @@ -0,0 +1,35 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.infrastructure; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Stereotype; + +@Stereotype +@Alternative +@Retention(RUNTIME) +@Target({TYPE, METHOD, FIELD}) +public @interface CdiMock { + +} diff --git a/apps/customer-service/src/test/resources/META-INF/beans.xml b/apps/customer-service/src/test/resources/META-INF/beans.xml new file mode 100644 index 0000000..4c50327 --- /dev/null +++ b/apps/customer-service/src/test/resources/META-INF/beans.xml @@ -0,0 +1,11 @@ + + + + rocks.limburg.cdimock.CdiMock + + diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json new file mode 100644 index 0000000..c44ac67 --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json @@ -0,0 +1,12 @@ +{ + "name": "Erika Mustermann", + "number": "0816", + "deliveryAddress": { + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" + } +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json new file mode 100644 index 0000000..6b677ea --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json @@ -0,0 +1,4 @@ +{ + "name": "Erika Mustermann", + "number": "0816" +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json new file mode 100644 index 0000000..b43e0fc --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json @@ -0,0 +1,12 @@ +{ + "name": "James Bond", + "number": "007", + "billingAddress": { + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" + } +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json new file mode 100644 index 0000000..368ad80 --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json @@ -0,0 +1,4 @@ +{ + "name": "James Bond", + "number": "007" +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json new file mode 100644 index 0000000..743b7e0 --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json @@ -0,0 +1,20 @@ +{ + "name": "Max Mustermann", + "number": "0815", + "billingAddress": { + "recipient": "Max Mustermann", + "street": { + "name": "Poststrasse", + "number": "1" + }, + "city": "26122 Oldenburg" + }, + "deliveryAddress": { + "recipient": "Max Mustermann", + "street": { + "name": "Poststrasse", + "number": "1" + }, + "city": "26122 Oldenburg" + } +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json new file mode 100644 index 0000000..872bbff --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json @@ -0,0 +1,4 @@ +{ + "name": "Max Mustermann", + "number": "0815" +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json new file mode 100644 index 0000000..481cc89 --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json @@ -0,0 +1,8 @@ +{ + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" +} diff --git a/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json new file mode 100644 index 0000000..84e0f5f --- /dev/null +++ b/apps/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json @@ -0,0 +1,3 @@ +{ + "name": "Sherlock Holmes" +} diff --git a/apps/delivery-service/.gitignore b/apps/delivery-service/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/apps/delivery-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/apps/delivery-service/Dockerfile b/apps/delivery-service/Dockerfile new file mode 100644 index 0000000..8623310 --- /dev/null +++ b/apps/delivery-service/Dockerfile @@ -0,0 +1,8 @@ +FROM openjdk:11-jre + +RUN wget https://repo.maven.apache.org/maven2/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar -O /opt/jaxb-api.jar +RUN wget https://repo.maven.apache.org/maven2/org/apache/meecrowave/meecrowave-core/1.2.13/meecrowave-core-1.2.13-runner.jar -O /opt/meecrowave-core-runner.jar +ADD target/delivery-service.war /opt/delivery-service.war + +EXPOSE 4002 +ENTRYPOINT ["java", "--illegal-access=permit", "-Djava.net.preferIPv4Stack=true", "-cp", "/opt/meecrowave-core-runner.jar:/opt/jaxb-api.jar", "org.apache.meecrowave.runner.Cli", "--http", "4002", "--webapp","/opt/delivery-service.war"] diff --git a/apps/delivery-service/Jenkinsfile b/apps/delivery-service/Jenkinsfile new file mode 100644 index 0000000..a31ff64 --- /dev/null +++ b/apps/delivery-service/Jenkinsfile @@ -0,0 +1,125 @@ +#!/usr/bin/env groovy +pipeline { + agent any + + options { + disableConcurrentBuilds() + } + + environment { + SNAPSHOT_VERSION = readMavenPom().getVersion() + LAST_COMMIT_MESSAGE = "${currentBuild.changeSets.size() == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length == 0 ? 'update version to ' : currentBuild.changeSets[currentBuild.changeSets.size() - 1].items[currentBuild.changeSets[currentBuild.changeSets.size() - 1].items.length - 1].msg}" + PERFORM_RELEASE = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') && env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ')}" + RELEASE_VERSION = "${env.SNAPSHOT_VERSION.contains('-SNAPSHOT') ? env.SNAPSHOT_VERSION.substring(0, env.SNAPSHOT_VERSION.lastIndexOf('-SNAPSHOT')) : SNAPSHOT_VERSION}" + VERSION = "${env.BRANCH_NAME == 'main' && !env.LAST_COMMIT_MESSAGE.startsWith('update version to ') ? env.RELEASE_VERSION : env.SNAPSHOT_VERSION}" + } + + triggers { + pollSCM("* * * * *") + } + + stages { + stage ('Compile') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + echo "Building version ${env.VERSION}" + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn versions:set -DnewVersion=${env.RELEASE_VERSION} -B" + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.RELEASE_VERSION}/g' deployment/overlays/prod/kustomization.yaml" + } else { + sh "sed -i 's/${env.SNAPSHOT_VERSION}/${env.GIT_COMMIT}/g' deployment/overlays/test/kustomization.yaml" + } + } + sh 'mvn clean test-compile -B' + } + } + stage ('Test') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh "mvn test -B" + } + } + stage ('Package') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh 'mvn package -DskipTests -B' + sh 'docker build -t delivery .' + } + } + stage ('Push') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + sh """ + docker tag delivery localhost:30010/delivery:${env.VERSION} + docker tag delivery localhost:30010/delivery:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker tag delivery localhost:30010/delivery:${env.GIT_COMMIT} + docker push localhost:30010/delivery:${env.VERSION} + docker push localhost:30010/delivery:${env.BRANCH_NAME == 'main' ? 'stable' : 'latest'} + docker push localhost:30010/delivery:${env.GIT_COMMIT} + """ + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh 'git config --global user.name "Jenkins"' + sh 'git config --global user.email "ci@openknowledge.de"' + sh "mvn scm:checkin -Dmessage='release of version ${env.RELEASE_VERSION}' -B" + sh "mvn scm:tag -Dtag=${env.RELEASE_VERSION} -B" + int nextRevision = Integer.parseInt(env.RELEASE_VERSION.substring(env.RELEASE_VERSION.lastIndexOf(".") + 1)) + 1 + nextVersion = RELEASE_VERSION.substring(0, env.RELEASE_VERSION.lastIndexOf(".")) + "." + nextRevision + "-SNAPSHOT" + sh "sed -i 's/${env.RELEASE_VERSION}/${nextVersion}/g' deployment/overlays/prod/kustomization.yaml" + sh "mvn versions:set scm:checkin -DnewVersion=${nextVersion} -Dmessage='update version to ${nextVersion}' -B" + } + } + } + } + stage ('Deploy') { + when { + anyOf { + not { + branch 'main' + } + environment name: 'PERFORM_RELEASE', value: 'true' + } + } + steps { + script { + if (env.PERFORM_RELEASE.equals('true') && !env.RELEASE_VERSION.equals(env.SNAPSHOT_VERSION)) { + sh "mvn scm:checkout -DscmVersion=${env.RELEASE_VERSION} -DscmVersionType=tag -B" + sh 'kubectl apply -k deployment/overlays/prod' + } else { + sh 'kubectl apply -k deployment/overlays/test' + sh "sed -i 's/${env.GIT_COMMIT}/${env.SNAPSHOT_VERSION}/g' deployment/overlays/test/kustomization.yaml" + } + } + } + } + } +} diff --git a/apps/delivery-service/README.md b/apps/delivery-service/README.md new file mode 100644 index 0000000..22324ef --- /dev/null +++ b/apps/delivery-service/README.md @@ -0,0 +1,18 @@ +# cdc-delivery-service + +Delivery Service to show Consumer-Driven Contracts + +## Installing the database + +Please run the following commands: +``` +docker build -t delivery-db -f Postgres-Dockerfile . +docker tag delivery-db localhost:5000/delivery-db:1.0.0 +docker tag delivery-db localhost:5000/delivery-db:latest +docker push localhost:5000/delivery-db:1.0.0 +docker push localhost:5000/delivery-db:latest + +cd helm-database/delivery-db/ +helm package . +helm install delivery-db --namespace onlineshop . +helm install delivery-db --namespace onlineshop-test . diff --git a/apps/delivery-service/deployment/base/deployment.yaml b/apps/delivery-service/deployment/base/deployment.yaml new file mode 100644 index 0000000..ed7759d --- /dev/null +++ b/apps/delivery-service/deployment/base/deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: delivery-deployment + labels: + app: delivery-service +spec: + replicas: 1 + selector: + matchLabels: + app: delivery-service + template: + metadata: + labels: + app: delivery-service + spec: + containers: + - name: delivery + image: delivery:latest + ports: + - containerPort: 4002 + name: http + env: + - name: ADDRESS_VALIDATION_SERVICE_URL + value: http://address-validation-service:4003 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: delivery-db-deployment + labels: + app: delivery-db +spec: + replicas: 1 + selector: + matchLabels: + app: delivery-db + template: + metadata: + labels: + app: delivery-db + spec: + containers: + - name: delivery-postgres + image: host.docker.internal:5000/delivery-db:local + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: delivery-service + - name: POSTGRES_PASSWORD + value: delivery-password diff --git a/apps/delivery-service/deployment/base/kustomization.yaml b/apps/delivery-service/deployment/base/kustomization.yaml new file mode 100644 index 0000000..5b98e94 --- /dev/null +++ b/apps/delivery-service/deployment/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml diff --git a/apps/delivery-service/deployment/base/service.yaml b/apps/delivery-service/deployment/base/service.yaml new file mode 100644 index 0000000..c5a2e9b --- /dev/null +++ b/apps/delivery-service/deployment/base/service.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: Service +metadata: + name: delivery-service +spec: + selector: + app: delivery-service + type: NodePort + ports: + - protocol: TCP + port: 4002 + targetPort: 4002 + nodePort: 30080 + name: service +--- +apiVersion: v1 +kind: Service +metadata: + name: delivery-db +spec: + selector: + app: delivery-db + type: NodePort + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + name: service diff --git a/apps/delivery-service/deployment/overlays/prod/kustomization.yaml b/apps/delivery-service/deployment/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..d64ca7e --- /dev/null +++ b/apps/delivery-service/deployment/overlays/prod/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: prod + +resources: + - ../../base + +patches: + - target: + version: v1 + kind: Service + name: delivery-service + path: ./patches/port-patch.yaml + +images: + - name: delivery + newName: localhost:30010/delivery + newTag: 1.1.0-SNAPSHOT diff --git a/apps/delivery-service/deployment/overlays/prod/patches/port-patch.yaml b/apps/delivery-service/deployment/overlays/prod/patches/port-patch.yaml new file mode 100644 index 0000000..1cce3de --- /dev/null +++ b/apps/delivery-service/deployment/overlays/prod/patches/port-patch.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: delivery-service +spec: + selector: + app: delivery-service + type: NodePort + ports: + - protocol: TCP + port: 4002 + targetPort: 4002 + nodePort: 31080 + name: service diff --git a/apps/delivery-service/deployment/overlays/test/kustomization.yaml b/apps/delivery-service/deployment/overlays/test/kustomization.yaml new file mode 100644 index 0000000..b9d9813 --- /dev/null +++ b/apps/delivery-service/deployment/overlays/test/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: test + +resources: + - ../../base + +images: + - name: delivery + newName: localhost:30010/delivery + newTag: 1.1.0-SNAPSHOT diff --git a/apps/delivery-service/namespaces.yaml b/apps/delivery-service/namespaces.yaml new file mode 100644 index 0000000..ab5c6df --- /dev/null +++ b/apps/delivery-service/namespaces.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: prod + labels: + name: prod +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test + labels: + name: test diff --git a/apps/delivery-service/pom.xml b/apps/delivery-service/pom.xml new file mode 100644 index 0000000..50b3017 --- /dev/null +++ b/apps/delivery-service/pom.xml @@ -0,0 +1,173 @@ + + + + + 4.0.0 + + de.openkonwledge.sample.shop + delivery-service + 1.1.0-SNAPSHOT + Microservices – Service "Delivery" + war + + + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/delivery-service.git + scm:git:http://openknowledge:workshop@gogs-service:3000/openknowledge/delivery-service.git + + + + 11 + 11 + false + UTF-8 + 1.2.13 + 5.4.21.Final + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.meecrowave + meecrowave-core + ${meecrowave.version} + provided + + + org.eclipse.microprofile.config + microprofile-config-api + 1.3 + + + org.microjpa + microjpa + 1.2.2 + + + org.hibernate + hibernate-entitymanager + ${hibernate.version} + + + org.postgresql + postgresql + 42.2.5 + + + org.apache.commons + commons-lang3 + 3.9 + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + org.hibernate + hibernate-c3p0 + ${hibernate.version} + test + + + com.h2database + h2 + 2.1.210 + test + + + au.com.dius.pact.consumer + junit5 + 4.3.5 + test + + + au.com.dius.pact.provider + junit5 + 4.3.5 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + rocks.limburg.cdimock + cdimock + 1.0.4 + test + + + org.apache.geronimo.config + geronimo-config-impl + 1.2.2 + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + + + delivery-service + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + ${project.version} + jdbc:h2:mem:delivery + org.h2.Driver + sa + sa + + + + + au.com.dius.pact.provider + maven + 4.3.5 + + http://localhost + + + + + diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java new file mode 100644 index 0000000..293581e --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class AddressesApplication extends Application { +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java new file mode 100644 index 0000000..d9b4e5d --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.AddressValidationService; +import de.openknowledge.sample.address.domain.AddressesRepository; +import de.openknowledge.sample.address.domain.CustomerNumber; + +/** + * RESTFul endpoint for delivery addresses + */ +@ApplicationScoped +@Path("/delivery-addresses") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AddressesResource { + + private static final Logger LOG = Logger.getLogger(AddressesResource.class.getSimpleName()); + + @Inject + private AddressValidationService addressValidationService; + @Inject + private AddressesRepository addressesRepository; + + @GET + @Path("/{customerNumber}") + @Produces(MediaType.APPLICATION_JSON) + public Address getAddress(@PathParam("customerNumber") CustomerNumber number) { + LOG.info("RESTful call 'GET address'"); + return addressesRepository.find(number).orElseThrow(NotFoundException::new); + } + + @POST + @Path("/{customerNumber}") + @Consumes(MediaType.APPLICATION_JSON) + public Response setAddress(@PathParam("customerNumber") CustomerNumber customerNumber, Address address, + @Context UriInfo uri) { + LOG.info("RESTful call 'POST address'"); + addressValidationService.validate(address); + addressesRepository.update(customerNumber, address); + return Response.ok().build(); + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 0000000..1847c83 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.AttributeOverride; +import javax.persistence.AttributeOverrides; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "ADDRESSES") +public class Address { + + @EmbeddedId + CustomerNumber id; + @Embedded + private Recipient recipient; + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "name.name", column = @Column(name = "STREET")), + @AttributeOverride(name = "number.number", column = @Column(name = "HOUSE_NUMBER")), + }) + private Street street; + @Embedded + private City city; + + protected Address() { + // for JPA + } + + @JsonbCreator + public Address(@JsonbProperty("recipient") Recipient recipient) { + this.recipient = notNull(recipient, "recipient may not be null"); + } + + public Address(Recipient recipient, Street street, City city) { + this(recipient); + setStreet(street); + setCity(city); + } + + public Recipient getRecipient() { + return recipient; + } + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java new file mode 100644 index 0000000..f55584e --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.AddressLine.Adapter; + +import static org.apache.commons.lang3.Validate.notNull; + +@JsonbTypeAdapter(Adapter.class) +public class AddressLine { + + public static final AddressLine EMPTY = new AddressLine(""); + + private String line; + + protected AddressLine() { + // for frameworks + } + + public AddressLine(String line) { + this.line = notNull(line, "line may not be null").trim(); + } + + public StreetName getStreetName() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new StreetName(line.substring(0, line.length() - lastSegment.length())); + } else if (containsDigit(firstSegment)) { + return new StreetName(line.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine street name"); + } + } + + public HouseNumber getHouseNumber() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new HouseNumber(lastSegment); + } else if (containsDigit(firstSegment)) { + return new HouseNumber(firstSegment); + } else { + throw new IllegalStateException("Could not determine house number"); + } + } + + @Override + public String toString() { + return line; + } + + + @Override + public int hashCode() { + return line.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AddressLine)) { + return false; + } + + AddressLine recipient = (AddressLine) object; + + return toString().equals(recipient.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public AddressLine adaptFromJson(String name) throws Exception { + return new AddressLine(name); + } + + @Override + public String adaptToJson(AddressLine name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java new file mode 100644 index 0000000..b322535 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java @@ -0,0 +1,44 @@ +package de.openknowledge.sample.address.domain; + +import static javax.ws.rs.client.Entity.entity; + +import java.io.StringReader; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.validation.ValidationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class AddressValidationService { + + private static final Logger LOG = Logger.getLogger(AddressValidationService.class.getSimpleName()); + private static final String ADDRESS_VALIDATION_PATH = "valid-addresses"; + + @Inject + @ConfigProperty(name = "address-validation-service.url") + String addressValidationServiceUrl; + + public void validate(Address address) { + Response validationResult = ClientBuilder + .newClient() + .register(JsonbJaxrsProvider.class) + .target(addressValidationServiceUrl) + .path(ADDRESS_VALIDATION_PATH) + .request(MediaType.APPLICATION_JSON) + .post(entity(address, MediaType.APPLICATION_JSON_TYPE)); + if (validationResult.getStatus() != Response.Status.OK.getStatusCode()) { + LOG.info("validation failed"); + JsonObject problem = Json.createReader(new StringReader(validationResult.readEntity(String.class))).readObject(); + throw new ValidationException(problem.getString("detail")); + } + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressesRepository.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressesRepository.java new file mode 100644 index 0000000..b594e46 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressesRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static javax.persistence.PersistenceContextType.EXTENDED; + +import java.util.Optional; + +import javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; + +/** + * Addresses repository + */ +@ApplicationScoped +public class AddressesRepository { + + @PersistenceContext(unitName = "delivery-service", type = EXTENDED) + private EntityManager entityManager; + + public Optional
find(CustomerNumber number) { + return Optional.ofNullable(entityManager.find(Address.class, number)); + } + + @Transactional + public void update(CustomerNumber number, Address address) { + address.id = number; + entityManager.merge(address); + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 0000000..2778fa0 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@Embeddable +@JsonbTypeAdapter(Adapter.class) +public class City { + + @Column(name = "CITY") + private String name; + + protected City() { + // for framework + } + + public static City valueOf(String name) { + return new City(name); + } + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + public ZipCode getZipCode() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new ZipCode(firstSegment); + } else if (containsDigit(lastSegment)) { + return new ZipCode(name.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine zip code"); + } + } + + public CityName getCityName() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new CityName(name.substring(firstSegment.length())); + } else if (containsDigit(lastSegment)) { + return new CityName(name.substring(0, firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine city name"); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City) object; + + return toString().equals(city.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 0000000..d96a15a --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + protected CityName() { + // for frameworks + } + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName) object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java new file mode 100644 index 0000000..3a6f90d --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import java.io.Serializable; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import de.openknowledge.sample.address.domain.CustomerNumber.Adapter; + +@Embeddable +@JsonbTypeAdapter(Adapter.class) +public class CustomerNumber implements Serializable { + + @Column(name = "ID") + private String number; + + protected CustomerNumber() { + // for frameworks + } + + public CustomerNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof CustomerNumber)) { + return false; + } + + CustomerNumber number = (CustomerNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerNumber adaptFromJson(String number) throws Exception { + return new CustomerNumber(number); + } + + @Override + public String adaptToJson(CustomerNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 0000000..a839f93 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.Embeddable; + +import static org.apache.commons.lang3.Validate.notBlank; + +@Embeddable +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + private String number; + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber number = (HouseNumber) object; + + return toString().equals(number.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java new file mode 100644 index 0000000..ffb0f5d --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Location { + + private ZipCode zipCode; + private CityName cityName; + + protected Location() { + // for frameworks + } + + @JsonbCreator + public Location(@JsonbProperty("zipCode") ZipCode zipCode, @JsonbProperty("cityName") CityName city) { + this.zipCode = notNull(zipCode, "zip code may not be null"); + this.cityName = notNull(city, "city name may not be null"); + } + + public ZipCode getZipCode() { + return zipCode; + } + + public CityName getCityName() { + return cityName; + } + + @Override + public int hashCode() { + return zipCode.hashCode() ^ cityName.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Location)) { + return false; + } + + Location location = (Location) object; + + return zipCode.equals(location.zipCode) && cityName.equals(location.cityName); + } + + @Override + public String toString() { + return zipCode.isGerman() ? zipCode + " " + cityName : cityName + " " + zipCode; + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java new file mode 100644 index 0000000..9cc98c8 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import de.openknowledge.sample.address.domain.Recipient.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@Embeddable +@JsonbTypeAdapter(Adapter.class) +public class Recipient { + + @Column(name = "RECIPIENT") + private String name; + + public static Recipient valueOf(String name) { + return new Recipient(name); + } + + protected Recipient() { + // for frameworks + } + + public Recipient(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + + @Override + public String toString() { + return name; + } + + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Recipient)) { + return false; + } + + Recipient recipient = (Recipient) object; + + return toString().equals(recipient.toString()); + } + + + public static class Adapter implements JsonbAdapter { + + @Override + public Recipient adaptFromJson(String name) throws Exception { + return new Recipient(name); + } + + @Override + public String adaptToJson(Recipient name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 0000000..9cdec68 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; + +@Embeddable +public class Street { + + @Embedded + private StreetName name; + @Embedded + private HouseNumber number; + + protected Street() { + // for frameworks + } + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street) object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 0000000..21026b4 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.persistence.Embeddable; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +import static org.apache.commons.lang3.Validate.notBlank; + +@Embeddable +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName)) { + return false; + } + StreetName name = (StreetName) object; + + return toString().equals(name.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 0000000..414d668 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + protected ZipCode() { + // for frameworks + } + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode code = (ZipCode) object; + + return toString().equals(code.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java new file mode 100644 index 0000000..fbfce55 --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CORSFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CORSFilter implements ContainerResponseFilter { + + @Override + public void filter(final ContainerRequestContext requestContext, + final ContainerResponseContext cres) throws IOException { + cres.getHeaders().add("Access-Control-Allow-Origin", "*"); + cres.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + cres.getHeaders().add("Access-Control-Allow-Credentials", "true"); + cres.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + cres.getHeaders().add("Access-Control-Max-Age", "1209600"); + } + +} diff --git a/apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java new file mode 100644 index 0000000..8eafb1d --- /dev/null +++ b/apps/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -0,0 +1,34 @@ +package de.openknowledge.sample.address.infrastructure; + +import javax.enterprise.context.ApplicationScoped; +import javax.validation.ValidationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +@ApplicationScoped +public class ValidationExceptionHandler implements ExceptionMapper { + + private static final String PROBLEM_JSON_TYPE = "application/problem+json"; + private static final String PROBLEM_JSON + = "{\"type\": \"%s\", \"title\": \"%s\", \"status\": %d, \"detail\": \"%s\", \"instance\": \"%s\"}"; + + @Context + private UriInfo uri; + + @Override + public Response toResponse(ValidationException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .type(PROBLEM_JSON_TYPE) + .entity(String.format( + PROBLEM_JSON, + uri.getBaseUri().resolve("/errors/invalid-" + uri.getPathSegments().get(uri.getPathSegments().size() - 1)), + "bad request", Response.Status.BAD_REQUEST.getStatusCode(), + exception.getMessage(), + uri.getAbsolutePath().toString())) + .build(); + } +} diff --git a/apps/delivery-service/src/main/resources/META-INF/microprofile-config.properties b/apps/delivery-service/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 0000000..26b88bb --- /dev/null +++ b/apps/delivery-service/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1 @@ +address-validation-service.url=http://localhost:4003 diff --git a/apps/delivery-service/src/main/resources/META-INF/persistence.xml b/apps/delivery-service/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..232c692 --- /dev/null +++ b/apps/delivery-service/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/apps/delivery-service/src/main/resources/sql/create.sql b/apps/delivery-service/src/main/resources/sql/create.sql new file mode 100644 index 0000000..950c434 --- /dev/null +++ b/apps/delivery-service/src/main/resources/sql/create.sql @@ -0,0 +1 @@ +CREATE TABLE IF NOT EXISTS ADDRESSES (ID VARCHAR(255) NOT NULL, RECIPIENT VARCHAR(255), STREET VARCHAR(255), HOUSE_NUMBER VARCHAR(255), CITY VARCHAR(255), PRIMARY KEY (ID)); diff --git a/apps/delivery-service/src/test/java/de/openknowledge/sample/address/DeliveryAddressServiceTest.java b/apps/delivery-service/src/test/java/de/openknowledge/sample/address/DeliveryAddressServiceTest.java new file mode 100644 index 0000000..fb2e877 --- /dev/null +++ b/apps/delivery-service/src/test/java/de/openknowledge/sample/address/DeliveryAddressServiceTest.java @@ -0,0 +1,97 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.openknowledge.sample.address; + +import static de.openknowledge.sample.infrastructure.H2DatabaseCleanup.with; +import static de.openknowledge.sample.infrastructure.ScriptExecutor.executeWith; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doThrow; + +import java.util.stream.Stream; + +import javax.enterprise.inject.Any; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.Tuple; +import javax.transaction.Status; +import javax.transaction.UserTransaction; +import javax.validation.ValidationException; + +import org.apache.meecrowave.Meecrowave; +import org.apache.meecrowave.junit5.MonoMeecrowaveConfig; +import org.apache.meecrowave.testing.ConfigurationInject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import au.com.dius.pact.provider.junit5.HttpTestTarget; +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.State; +import au.com.dius.pact.provider.junitsupport.StateChangeAction; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; +import de.openknowledge.sample.address.domain.AddressValidationService; +import rocks.limburg.cdimock.MockitoBeans; + +@MockitoBeans(types = {AddressValidationService.class}) +@Provider("delivery-service") +@PactFolder("src/test/pacts") +@MonoMeecrowaveConfig +public class DeliveryAddressServiceTest { + + @ConfigurationInject + private Meecrowave.Builder config; + + @Inject + private AddressValidationService addressValidationService; + @Inject + private UserTransaction transaction; + @Inject @Any + private EntityManager entityManager; + + @BeforeEach + public void setUp(PactVerificationContext context) { + doThrow(new ValidationException("City not found")).when(addressValidationService).validate(argThat(a -> a.getCity().toString().contains("London"))); + context.setTarget(new HttpTestTarget("localhost", config.getHttpPort(), "/")); + } + + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + context.verifyInteraction(); + } + + @State("Three customers") + public void setThreeCustomers() throws Exception { + transaction.begin(); + executeWith(entityManager) + .script("sql/create.sql") + .script("sql/data.sql"); + transaction.commit(); + } + + @State(value = "Three customers", action = StateChangeAction.TEARDOWN) + public void cleanupThreeCustomers() throws Exception { + if (transaction.getStatus() != Status.STATUS_ACTIVE) { + transaction.begin(); + } + with(entityManager).clearDatabase(); + transaction.commit(); + } +} + diff --git a/apps/delivery-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java b/apps/delivery-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java new file mode 100644 index 0000000..32f1d9c --- /dev/null +++ b/apps/delivery-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java @@ -0,0 +1,41 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address; + +import java.io.InputStream; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; + +import org.assertj.core.api.Condition; + +public class JsonObjectComparision extends Condition> { + + public JsonObjectComparision(JsonObject object) { + super(v -> v.entrySet().containsAll(object.entrySet()), "object containing %s", object); + } + + public static Condition> sameAs(InputStream in) { + return new JsonObjectComparision(Json.createReader(in).readObject()); + } + + public static Condition thatIsSameAs(InputStream in) { + Condition condition = new JsonObjectComparision(Json.createReader(in).readObject()); + return (Condition)condition; + } +} diff --git a/apps/delivery-service/src/test/java/de/openknowledge/sample/address/domain/AddressValidationServiceTest.java b/apps/delivery-service/src/test/java/de/openknowledge/sample/address/domain/AddressValidationServiceTest.java new file mode 100644 index 0000000..6c90eb1 --- /dev/null +++ b/apps/delivery-service/src/test/java/de/openknowledge/sample/address/domain/AddressValidationServiceTest.java @@ -0,0 +1,112 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.openknowledge.sample.address.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; + +import javax.validation.ValidationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.dsl.PactDslJsonBody; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; +import au.com.dius.pact.consumer.junit5.PactTestFor; +import au.com.dius.pact.core.model.PactSpecVersion; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.annotations.Pact; + +@ExtendWith(PactConsumerTestExt.class) +@PactTestFor(providerName = "address-validation-service", pactVersion = PactSpecVersion.V3) +public class AddressValidationServiceTest { + + private AddressValidationService service; + + @Pact(consumer = "delivery-service") + public RequestResponsePact validateMaxsAddress(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("POST request for Max's address") + .path("/valid-addresses") + .method("POST") + .matchHeader("Content-Type", "application/json.*", "application/json") + .body(new PactDslJsonBody() + .stringValue("recipient", "Max Mustermann") + .stringValue("city", "26122 Oldenburg") + .object("street") + .stringValue("name", "Poststrasse") + .stringValue("number", "1") + .closeObject()) + .willRespondWith() + .status(200) + .toPact(); + } + + @Pact(consumer = "delivery-service") + public RequestResponsePact validateSherlocksAddress(PactDslWithProvider builder) throws IOException { + return builder + .given("Three customers") + .uponReceiving("POST request for Sherlock's address") + .path("/valid-addresses") + .method("POST") + .matchHeader("Content-Type", "application/json.*", "application/json") + .body(new PactDslJsonBody() + .stringValue("recipient", "Sherlock Holmes") + .stringValue("city", "London NW1 6XE") + .object("street") + .stringValue("name", "Baker Street") + .stringValue("number", "221B") + .closeObject()) + .willRespondWith() + .status(400) + .matchHeader("Content-Type", "application/problem\\+json.*", "application/problem+json") + .body(new PactDslJsonBody().stringMatcher("detail", ".*", "Addresses from UK are not supported for delivery")) + .toPact(); + } + + @BeforeEach + public void initializeService(MockServer mockServer) { + service = new AddressValidationService(); + service.addressValidationServiceUrl = "http://localhost:" + mockServer.getPort(); + } + + @PactTestFor(pactMethod = "validateMaxsAddress") + @Test + public void validAddress() { + service.validate(new Address( + new Recipient("Max Mustermann"), + new Street(new StreetName("Poststrasse"), new HouseNumber("1")), + new City("26122 Oldenburg"))); + } + + @Test + @PactTestFor(pactMethod = "validateSherlocksAddress") + public void invalidAddress() { + assertThatThrownBy(() -> + service.validate(new Address( + new Recipient("Sherlock Holmes"), + new Street(new StreetName("Baker Street"), new HouseNumber("221B")), + new City("London NW1 6XE")))) + .isInstanceOf(ValidationException.class) + .hasMessage("Addresses from UK are not supported for delivery"); + } +} diff --git a/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java b/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java new file mode 100644 index 0000000..fdc16d0 --- /dev/null +++ b/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java @@ -0,0 +1,35 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.infrastructure; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Stereotype; + +@Stereotype +@Alternative +@Retention(RUNTIME) +@Target({TYPE, METHOD, FIELD}) +public @interface CdiMock { + +} diff --git a/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/H2DatabaseCleanup.java b/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/H2DatabaseCleanup.java new file mode 100644 index 0000000..1092550 --- /dev/null +++ b/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/H2DatabaseCleanup.java @@ -0,0 +1,41 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.infrastructure; + +import java.util.stream.Stream; + +import javax.persistence.EntityManager; +import javax.persistence.Tuple; + +public class H2DatabaseCleanup { + + private EntityManager entityManager; + + public H2DatabaseCleanup(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public static H2DatabaseCleanup with(EntityManager entityManager) { + return new H2DatabaseCleanup(entityManager); + } + + public void clearDatabase() { + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + Stream tables = (Stream)entityManager.createNativeQuery("SHOW TABLES", Tuple.class).getResultList().stream(); + tables.map(table -> table.get(0).toString()).forEach(table -> entityManager.createNativeQuery("TRUNCATE TABLE " + table).executeUpdate()); + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } +} diff --git a/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/ScriptExecutor.java b/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/ScriptExecutor.java new file mode 100644 index 0000000..94e9b5a --- /dev/null +++ b/apps/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/ScriptExecutor.java @@ -0,0 +1,43 @@ +/* + * Copyright open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.infrastructure; + +import java.io.IOException; + +import javax.persistence.EntityManager; + +import org.apache.commons.io.IOUtils; + +public class ScriptExecutor { + + private EntityManager entityManager; + + public ScriptExecutor(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public static ScriptExecutor executeWith(EntityManager entityManager) { + return new ScriptExecutor(entityManager); + } + + public ScriptExecutor script(String name) throws IOException { + String script = IOUtils.toString(Thread.currentThread().getContextClassLoader().getResourceAsStream(name)); + for (String line: script.split(";")) { + entityManager.createNativeQuery(line).executeUpdate(); + } + return this; + } +} diff --git a/apps/delivery-service/src/test/pacts/customer-service-delivery-service.json b/apps/delivery-service/src/test/pacts/customer-service-delivery-service.json new file mode 100644 index 0000000..bea7948 --- /dev/null +++ b/apps/delivery-service/src/test/pacts/customer-service-delivery-service.json @@ -0,0 +1,180 @@ +{ + "provider": { + "name": "delivery-service" + }, + "consumer": { + "name": "customer-service" + }, + "interactions": [ + { + "description": "GET request for 0815", + "request": { + "method": "GET", + "path": "/delivery-addresses/0815" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json; charset=UTF-8" + }, + "body": { + "city": "26122 Oldenburg", + "street": { + "number": "1", + "name": "Poststr." + }, + "recipient": "Max Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json(;\\s?charset=[\\w\\-]+)?" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "GET request for 0817", + "request": { + "method": "GET", + "path": "/delivery-addresses/0817" + }, + "response": { + "status": 404 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for 007", + "request": { + "method": "POST", + "path": "/delivery-addresses/007", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "London NW1 6XE", + "street": { + "number": "221B", + "name": "Baker Street" + }, + "recipient": "Sherlock Holmes" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/problem+json" + }, + "body": { + "detail": "Addresses from UK are not supported for delivery" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/problem\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.detail": { + "matchers": [ + { + "match": "regex", + "regex": ".*" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for 0815", + "request": { + "method": "POST", + "path": "/delivery-addresses/0815", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "45127 Essen", + "street": { + "number": "7", + "name": "II. Hagen" + }, + "recipient": "Erika Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + } + ], + "metadata": { + "pactSpecification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "4.1.7" + } + } +} diff --git a/apps/delivery-service/src/test/resources/c3p0.properties b/apps/delivery-service/src/test/resources/c3p0.properties new file mode 100644 index 0000000..06bef26 --- /dev/null +++ b/apps/delivery-service/src/test/resources/c3p0.properties @@ -0,0 +1 @@ +c3p0.testConnectionOnCheckout=true diff --git a/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json new file mode 100644 index 0000000..481cc89 --- /dev/null +++ b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json @@ -0,0 +1,8 @@ +{ + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" +} diff --git a/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json new file mode 100644 index 0000000..9541839 --- /dev/null +++ b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json new file mode 100644 index 0000000..84bb8d0 --- /dev/null +++ b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json @@ -0,0 +1,8 @@ +{ + "recipient": "Max Mustermann", + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 Oldenburg" +} diff --git a/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json new file mode 100644 index 0000000..58068cc --- /dev/null +++ b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json @@ -0,0 +1,8 @@ +{ + "recipient": "Max Mustermann", + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 London" +} diff --git a/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json new file mode 100644 index 0000000..9541839 --- /dev/null +++ b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json new file mode 100644 index 0000000..77c04eb --- /dev/null +++ b/apps/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} diff --git a/apps/delivery-service/src/test/resources/h2-ds.xml b/apps/delivery-service/src/test/resources/h2-ds.xml new file mode 100644 index 0000000..b652c1e --- /dev/null +++ b/apps/delivery-service/src/test/resources/h2-ds.xml @@ -0,0 +1,20 @@ + + + + ${database.url} + h2 + + 4 + 4 + 64 + + + sa + sa + + + diff --git a/apps/delivery-service/src/test/resources/sql/data.sql b/apps/delivery-service/src/test/resources/sql/data.sql new file mode 100644 index 0000000..6d35447 --- /dev/null +++ b/apps/delivery-service/src/test/resources/sql/data.sql @@ -0,0 +1,2 @@ +INSERT INTO ADDRESSES (ID, RECIPIENT, STREET, HOUSE_NUMBER, CITY) VALUES ('0815', 'Max Mustermann', 'Poststr.', '1', '26122 Oldenburg'); +INSERT INTO ADDRESSES (ID, RECIPIENT, STREET, HOUSE_NUMBER, CITY) VALUES ('0816', 'Erika Mustermann', 'II. Hagen', '7', '45127 Essen'); diff --git a/delivery-db/Dockerfile b/delivery-db/Dockerfile new file mode 100644 index 0000000..965488b --- /dev/null +++ b/delivery-db/Dockerfile @@ -0,0 +1,2 @@ +FROM postgres:15.4-bullseye +COPY apps/delivery-service/src/main/resources/sql/create.sql /docker-entrypoint-initdb.d/create.sql diff --git a/deployment/base/gogs/deployment.yaml b/deployment/base/gogs/deployment.yaml index 55d6cba..c3b0b38 100644 --- a/deployment/base/gogs/deployment.yaml +++ b/deployment/base/gogs/deployment.yaml @@ -20,3 +20,28 @@ spec: ports: - containerPort: 3000 name: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gogs-db-deployment + labels: + app: gogs-db-service +spec: + replicas: 1 + selector: + matchLabels: + app: gogs-db-service + template: + metadata: + labels: + app: gogs-db-service + spec: + containers: + - name: gogs-postgres + image: postgres:15.4-bullseye + ports: + - containerPort: 5432 + env: + - name: POSTGRES_PASSWORD + value: g0g5db diff --git a/deployment/base/gogs/ingress.yaml b/deployment/base/gogs/ingress.yaml index e721a3d..14cb82f 100644 --- a/deployment/base/gogs/ingress.yaml +++ b/deployment/base/gogs/ingress.yaml @@ -16,4 +16,4 @@ spec: service: name: gogs-service port: - number: 30090 + number: 30030 diff --git a/deployment/base/gogs/service.yaml b/deployment/base/gogs/service.yaml index c271e17..10e7c6e 100644 --- a/deployment/base/gogs/service.yaml +++ b/deployment/base/gogs/service.yaml @@ -10,5 +10,19 @@ spec: - protocol: TCP port: 3000 targetPort: 3000 - nodePort: 30060 + nodePort: 30030 + name: service +--- +apiVersion: v1 +kind: Service +metadata: + name: gogs-db-service +spec: + selector: + app: gogs-db-service + type: NodePort + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 name: service diff --git a/deployment/base/jenkins/deployment.yaml b/deployment/base/jenkins/deployment.yaml index e02aff3..f6b3228 100644 --- a/deployment/base/jenkins/deployment.yaml +++ b/deployment/base/jenkins/deployment.yaml @@ -20,3 +20,10 @@ spec: ports: - containerPort: 8080 name: http + volumeMounts: + - mountPath: /var/run + name: docker-sock + volumes: + - name: docker-sock + hostPath: + path: /var/run diff --git a/deployment/base/jenkins/ingress.yaml b/deployment/base/jenkins/ingress.yaml index 25703f4..d73f18d 100644 --- a/deployment/base/jenkins/ingress.yaml +++ b/deployment/base/jenkins/ingress.yaml @@ -16,4 +16,4 @@ spec: service: name: jenkins-service port: - number: 30090 + number: 30040 diff --git a/deployment/base/jenkins/service.yaml b/deployment/base/jenkins/service.yaml index 1889019..83006e4 100644 --- a/deployment/base/jenkins/service.yaml +++ b/deployment/base/jenkins/service.yaml @@ -10,5 +10,5 @@ spec: - protocol: TCP port: 8080 targetPort: 8080 - nodePort: 30070 + nodePort: 30040 name: service diff --git a/deployment/base/kustomization.yaml b/deployment/base/kustomization.yaml index d16774b..14b02c0 100644 --- a/deployment/base/kustomization.yaml +++ b/deployment/base/kustomization.yaml @@ -2,7 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: + - ./namespaces.yaml - ./nginx + - ./registry - ./pact - ./gogs - ./jenkins diff --git a/deployment/base/namespaces.yaml b/deployment/base/namespaces.yaml new file mode 100644 index 0000000..ab5c6df --- /dev/null +++ b/deployment/base/namespaces.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: prod + labels: + name: prod +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test + labels: + name: test diff --git a/deployment/base/pact/deployment.yaml b/deployment/base/pact/deployment.yaml index 4e1b23f..3fe5a6e 100644 --- a/deployment/base/pact/deployment.yaml +++ b/deployment/base/pact/deployment.yaml @@ -27,3 +27,9 @@ spec: value: sqlite - name: PACT_BROKER_DATABASE_NAME value: pact_broker.sqlite + - name: PACT_BROKER_WEBHOOK_SCHEME_WHITELIST + value: http + - name: PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST + value: GET + - name: PACT_BROKER_WEBHOOK_HOST_WHITELIST + value: jenkins-service diff --git a/deployment/base/pact/ingress.yaml b/deployment/base/pact/ingress.yaml index 100e93b..e7a8a1d 100644 --- a/deployment/base/pact/ingress.yaml +++ b/deployment/base/pact/ingress.yaml @@ -16,4 +16,4 @@ spec: service: name: pact-service port: - number: 30080 + number: 30020 diff --git a/deployment/base/pact/service.yaml b/deployment/base/pact/service.yaml index 8011500..67e4d39 100644 --- a/deployment/base/pact/service.yaml +++ b/deployment/base/pact/service.yaml @@ -10,5 +10,5 @@ spec: - protocol: TCP port: 8080 targetPort: 8080 - nodePort: 30050 + nodePort: 30020 name: service diff --git a/deployment/base/registry/deployment.yaml b/deployment/base/registry/deployment.yaml new file mode 100644 index 0000000..acd8b8d --- /dev/null +++ b/deployment/base/registry/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: registry-deployment + labels: + app: registry-service +spec: + replicas: 1 + selector: + matchLabels: + app: registry-service + template: + metadata: + labels: + app: registry-service + spec: + containers: + - name: registry + image: registry:2.8.2 + ports: + - containerPort: 5000 + name: http diff --git a/deployment/base/registry/ingress.yaml b/deployment/base/registry/ingress.yaml new file mode 100644 index 0000000..650564f --- /dev/null +++ b/deployment/base/registry/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: registry-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: registry.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: registry-service + port: + number: 30010 diff --git a/deployment/base/registry/kustomization.yaml b/deployment/base/registry/kustomization.yaml new file mode 100644 index 0000000..350ec31 --- /dev/null +++ b/deployment/base/registry/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + - ingress.yaml \ No newline at end of file diff --git a/deployment/base/registry/service.yaml b/deployment/base/registry/service.yaml new file mode 100644 index 0000000..7b341e7 --- /dev/null +++ b/deployment/base/registry/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: registry-service +spec: + selector: + app: registry-service + type: NodePort + ports: + - protocol: TCP + port: 5000 + targetPort: 5000 + nodePort: 30010 + name: service diff --git a/deployment/cluster-config/kind-config.yml b/deployment/cluster-config/kind-config.yml index 8ce9c1d..f9ab44f 100644 --- a/deployment/cluster-config/kind-config.yml +++ b/deployment/cluster-config/kind-config.yml @@ -9,8 +9,17 @@ nodes: kubeletExtraArgs: node-labels: "ingress-ready=true" extraPortMappings: - - containerPort: 30050 - hostPort: 30050 + - containerPort: 30010 + hostPort: 30010 + protocol: TCP + - containerPort: 30020 + hostPort: 30020 + protocol: TCP + - containerPort: 30030 + hostPort: 30030 + protocol: TCP + - containerPort: 30040 + hostPort: 30040 protocol: TCP - containerPort: 30060 hostPort: 30060 @@ -18,3 +27,32 @@ nodes: - containerPort: 30070 hostPort: 30070 protocol: TCP + - containerPort: 30080 + hostPort: 30080 + protocol: TCP + - containerPort: 30090 + hostPort: 30090 + protocol: TCP + - containerPort: 31060 + hostPort: 31060 + protocol: TCP + - containerPort: 31070 + hostPort: 31070 + protocol: TCP + - containerPort: 31080 + hostPort: 31080 + protocol: TCP + - containerPort: 31090 + hostPort: 31090 + protocol: TCP + extraMounts: + - hostPath: /var/run/docker.sock + containerPath: /var/run/docker.sock + - role: worker + extraMounts: + - hostPath: /var/run/docker.sock + containerPath: /var/run/docker.sock + - role: worker + extraMounts: + - hostPath: /var/run/docker.sock + containerPath: /var/run/docker.sock diff --git a/docker-compose.yaml b/docker-compose.yaml index b4ae4ae..23ba72e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,5 +6,12 @@ services: build: jenkins/ image: host.docker.internal:5000/jenkins:local setup: - build: setup/ + build: + context: . + dockerfile: ./setup/Dockerfile image: host.docker.internal:5000/setup:local + delivery-db: + build: + context: . + dockerfile: ./delivery-db/Dockerfile + image: host.docker.internal:5000/delivery-db:local \ No newline at end of file diff --git a/gogs/Dockerfile b/gogs/Dockerfile index 88a38c2..166a883 100644 --- a/gogs/Dockerfile +++ b/gogs/Dockerfile @@ -1 +1,2 @@ -FROM gogs/gogs:0.13 \ No newline at end of file +FROM gogs/gogs:0.13 +COPY app.ini /data/gogs/conf/app.ini diff --git a/gogs/app.ini b/gogs/app.ini new file mode 100644 index 0000000..f8ae527 --- /dev/null +++ b/gogs/app.ini @@ -0,0 +1,15 @@ +[repository] +DEFAULT_BRANCH = main +[database] +TYPE = postgres +HOST = gogs-db-service:5432 +NAME = postgres +USER = postgres +PASSWORD = g0g5db +SCHEMA = public +SSL_MODE = disable +[auth] +REQUIRE_EMAIL_CONFIRMATION = false +ENABLE_REGISTRATION_CAPTCHA = false +[security] +INSTALL_LOCK = true diff --git a/jenkins/Dockerfile b/jenkins/Dockerfile index a6e29fc..411dbd1 100644 --- a/jenkins/Dockerfile +++ b/jenkins/Dockerfile @@ -1 +1,39 @@ FROM jenkins/jenkins:2.414.1-lts-jdk11 + +RUN jenkins-plugin-cli -p git workflow-aggregator pipeline-utility-steps generic-webhook-trigger pipeline-stage-view + +ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false + +USER root + +# increase number of executors +COPY --chown=jenkins:jenkins executors.groovy /usr/share/jenkins/ref/init.groovy.d/executors.groovy + +# install docker +RUN apt-get update -qq && apt-get install --no-install-recommends -qqy apt-transport-https ca-certificates curl gnupg2 software-properties-common\ + && curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -\ + && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"\ + && apt-get update -qq && apt-get install --no-install-recommends docker-ce -y\ + +# install kubectl + && apt-get update -qq && apt-get install --no-install-recommends -y apt-transport-https ca-certificates curl\ + && mkdir -p /etc/apt/keyrings\ + && curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg\ + && chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg\ + && echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list\ + && apt-get update -qq && apt-get install --no-install-recommends -qy kubectl kubelet kubeadm\ + +# install maven + && apt-get install --no-install-recommends -y maven\ + +# install pact client + && apt-get install --no-install-recommends -y curl\ + && curl -LO https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v1.69.0/pact-1.69.0-linux-x86_64.tar.gz && tar xzf pact-1.69.0-linux-x86_64.tar.gz -C /usr/local/ && ln -s /usr/local/pact/bin/pact-broker /usr/local/bin/pact-broker\ + && rm -rf /var/lib/apt/lists/* + +RUN usermod -aG docker jenkins + +# configure access to cluster +COPY kube-config /root/.kube/config + +#USER jenkins diff --git a/jenkins/executors.groovy b/jenkins/executors.groovy new file mode 100644 index 0000000..6c8324f --- /dev/null +++ b/jenkins/executors.groovy @@ -0,0 +1,2 @@ +import jenkins.model.* +Jenkins.instance.setNumExecutors(4) \ No newline at end of file diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..9e33b5a --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,2 @@ +/.tmp/* +!.gitignore diff --git a/scripts/cleanJobs.groovy b/scripts/cleanJobs.groovy new file mode 100644 index 0000000..c928277 --- /dev/null +++ b/scripts/cleanJobs.groovy @@ -0,0 +1,9 @@ +Jenkins.instance.getItems().each { + def pipeline = it + pipeline.allJobs.each { + def job = it + job.builds.each { it.delete() } + job.nextBuildNumber = 1 + job.save() + } +} \ No newline at end of file diff --git a/scripts/cleanJobs.sh b/scripts/cleanJobs.sh new file mode 100755 index 0000000..d91d60a --- /dev/null +++ b/scripts/cleanJobs.sh @@ -0,0 +1,7 @@ +#!/bin/bash +SCRIPTS_DIR=$(readlink -f $(dirname $0)) +cd $SCRIPTS_DIR + +JENKINS_URL="http://localhost:30070" + +./jenkinsCurl.sh -X POST --data-urlencode "script@$SCRIPTS_DIR/cleanJobs.groovy" $JENKINS_URL/scriptText diff --git a/scripts/jenkinsCurl.sh b/scripts/jenkinsCurl.sh new file mode 100755 index 0000000..d24c009 --- /dev/null +++ b/scripts/jenkinsCurl.sh @@ -0,0 +1,14 @@ +#!/bin/bash +SCRIPTS_DIR=$(readlink -f $(dirname $0)) +cd $SCRIPTS_DIR + +TMP_DIR=.tmp/jenkins +mkdir -p $TMP_DIR +cd $TMP_DIR + +JENKINS_URL="http://localhost:30040" + +COOKIE_FILE=./cookiefile +CRUMB=$(curl -c $COOKIE_FILE --silent "$JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)") +curl -b ./cookiefile -H "$CRUMB" $* + diff --git a/scripts/push.sh b/scripts/push.sh new file mode 100755 index 0000000..05cda72 --- /dev/null +++ b/scripts/push.sh @@ -0,0 +1,36 @@ +#!/bin/bash +SCRIPTS_DIR=$(readlink -f $(dirname $0)) +TMP_DIR=.tmp +APPS_DIR=$SCRIPTS_DIR/../apps + +REPO_BASE_URL=localhost:30030 +JENKINS_URL="http://localhost:30040" + +REPO_NAME=$1 +TARGET_BRANCH="${2:-main}" + +# get branch name of the current repo +cd $APPS_DIR +BRANCH_NAME=$(git symbolic-ref --short HEAD) + +cd $SCRIPTS_DIR/$TMP_DIR +# clean +rm -rf ./$REPO_NAME/* +rm -drf ./$REPO_NAME/.git +mkdir -p $REPO_NAME +cd ./$REPO_NAME + +git init --initial-branch=$TARGET_BRANCH +git config user.name openknowledge +git remote add origin http://openknowledge:workshop@$REPO_BASE_URL/openknowledge/$REPO_NAME + +cp -r $APPS_DIR/$REPO_NAME/* . +cp $APPS_DIR/$REPO_NAME/.gitignore ./ + +git add . +git commit -m "copy from workshop-repo branch $BRANCH_NAME" +git push --force --set-upstream origin $TARGET_BRANCH + +# trigger build on jenkins right now +$SCRIPTS_DIR/jenkinsCurl.sh -X POST "$JENKINS_URL/job/$REPO_NAME/build" + diff --git a/scripts/pushAll.sh b/scripts/pushAll.sh new file mode 100755 index 0000000..91d2745 --- /dev/null +++ b/scripts/pushAll.sh @@ -0,0 +1,11 @@ +#!/bin/bash +SCRIPTS_DIR=$(readlink -f $(dirname $0)) + +TARGET_BRANCH="${1:-main}" + +cd $SCRIPTS_DIR +./push.sh customer-service $TARGET_BRANCH +./push.sh billing-service $TARGET_BRANCH +./push.sh address-validation-service $TARGET_BRANCH +./push.sh delivery-service $TARGET_BRANCH + diff --git a/setup/Dockerfile b/setup/Dockerfile index a205006..050e16b 100644 --- a/setup/Dockerfile +++ b/setup/Dockerfile @@ -1,5 +1,9 @@ -FROM bash:5.2.15 +FROM alpine/git:2.40.1 -COPY setup.sh / +RUN apk --no-cache add curl bash +COPY .git /repo/.git +COPY setup/setup.sh / +COPY setup/pushToGogs.sh / +COPY setup/jenkins/ /jenkins -CMD ["bash", "/setup.sh"] +ENTRYPOINT ["sh", "/setup.sh"] diff --git a/setup/jenkins/address-validation-service/config.xml b/setup/jenkins/address-validation-service/config.xml new file mode 100644 index 0000000..2b25fb8 --- /dev/null +++ b/setup/jenkins/address-validation-service/config.xml @@ -0,0 +1,58 @@ + + + + + address-validation-service + + + + + + + + + + true + -1 + -1 + false + + + + * * * * * + 60000 + + + false + + + + + e873cb34-fb6f-4cf4-9ed3-b697a88c7f55 + http://gogs-service:3000/openknowledge/address-validation-service.git + + + + + main develop + + + + + ** + + + + + + + + + + + + + + Jenkinsfile + + \ No newline at end of file diff --git a/setup/jenkins/billing-service/config.xml b/setup/jenkins/billing-service/config.xml new file mode 100644 index 0000000..4f72231 --- /dev/null +++ b/setup/jenkins/billing-service/config.xml @@ -0,0 +1,58 @@ + + + + + billing-service + + + + + + + + + + true + -1 + -1 + false + + + + * * * * * + 60000 + + + false + + + + + e873cb34-fb6f-4cf4-9ed3-b697a88c7f55 + http://gogs-service:3000/openknowledge/billing-service.git + + + + + main develop + + + + + ** + + + + + + + + + + + + + + Jenkinsfile + + \ No newline at end of file diff --git a/setup/jenkins/create-jobs.sh b/setup/jenkins/create-jobs.sh new file mode 100755 index 0000000..069e98c --- /dev/null +++ b/setup/jenkins/create-jobs.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +JENKINS_URL="http://jenkins-service:8080" +# JENKINS_URL="http://localhost:30070" # for local testing + +#wait until jenkins is ready +curl --retry 100 -f --retry-all-errors --retry-delay 1 --silent "$JENKINS_URL/crumbIssuer/api/xml" >> /dev/null + +COOKIE_FILE=./cookiefile +CRUMB=$(curl -c $COOKIE_FILE --silent "$JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)") + +createjob() +{ + local NAME=$1 + if curl --fail --silent "$JENKINS_URL/job/$NAME"; then # check if job already exists + curl -b ./cookiefile -X POST -H "$CRUMB" "$JENKINS_URL/job/$NAME/doDelete" # delete old job + echo old job for $NAME was removed + fi + curl -b ./cookiefile -X POST -H "Content-Type:application/xml" -H "$CRUMB" -d @$NAME/config.xml "$JENKINS_URL/createItem?name=$NAME" + echo pipeline for $NAME created +} + +createjob customer-service +createjob address-validation-service +createjob billing-service +createjob delivery-service + +rm $COOKIE_FILE \ No newline at end of file diff --git a/setup/jenkins/customer-service/config.xml b/setup/jenkins/customer-service/config.xml new file mode 100644 index 0000000..f199a94 --- /dev/null +++ b/setup/jenkins/customer-service/config.xml @@ -0,0 +1,58 @@ + + + + + customer-service + + + + + + + + + + true + -1 + -1 + false + + + + * * * * * + 60000 + + + false + + + + + e873cb34-fb6f-4cf4-9ed3-c697a88c7f55 + http://gogs-service:3000/openknowledge/customer-service.git + + + + + main develop + + + + + ** + + + + + + + + + + + + + + Jenkinsfile + + \ No newline at end of file diff --git a/setup/jenkins/delivery-service/config.xml b/setup/jenkins/delivery-service/config.xml new file mode 100644 index 0000000..74498f7 --- /dev/null +++ b/setup/jenkins/delivery-service/config.xml @@ -0,0 +1,58 @@ + + + + + delivery-service + + + + + + + + + + true + -1 + -1 + false + + + + * * * * * + 60000 + + + false + + + + + e873cb34-fb6f-4cf4-9ed3-b697a88c7f55 + http://gogs-service:3000/openknowledge/delivery-service.git + + + + + main develop + + + + + ** + + + + + + + + + + + + + + Jenkinsfile + + \ No newline at end of file diff --git a/setup/pushToGogs.sh b/setup/pushToGogs.sh new file mode 100755 index 0000000..9e44690 --- /dev/null +++ b/setup/pushToGogs.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +REPO_BASE_URL=gogs-service:3000 + +BRANCHES=("pact" "pact-broker" "pipeline" "pact-tags" "webhook") +APPS=("address-validation-service" "billing-service" "customer-service" "delivery-service") + +REPO_DIR=/repo +TMP_DIR=/.tmp + +cd $REPO_DIR + +mkdir -p $TMP_DIR +for BRANCH in "${BRANCHES[@]}" +do + echo $(pwd) + git reset --hard origin/$BRANCH + rm -rf $TMP_DIR/* + cp -r $REPO_DIR/apps/* $TMP_DIR + for APP in "${APPS[@]}" + do + ( + echo "$BRANCH -> $APP" + cd $TMP_DIR/$APP + echo $(pwd) + git init --initial-branch=$BRANCH + git config user.name openknowledge + git config user.email "workshop@openknowledge.local" + git remote add origin http://openknowledge:workshop@$REPO_BASE_URL/openknowledge/$APP + git add . + git commit -m "copy from workshop-repo branch $BRANCH" + git push --force --set-upstream origin $BRANCH + ) + done +done diff --git a/setup/setup.sh b/setup/setup.sh old mode 100644 new mode 100755 index 81f2cc8..6155d21 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -1 +1,50 @@ -echo "Hello Setup" +echo "Creating Gogs user..." +curl \ + --retry 20 \ + -f \ + --retry-all-errors \ + --retry-delay 30 \ + -d "user_name=openknowledge&email=test@openknowledge.de&password=workshop&retype=workshop" \ + -X POST http://gogs-service:3000/user/sign_up \ + --silent --output /dev/null --show-error --fail +echo "Gogs user created." + +echo "Creating repositories..." + +curl \ + -f \ + -u "openknowledge:workshop" \ + -d "name=customer-service" \ + -X POST http://gogs-service:3000/api/v1/admin/users/openknowledge/repos \ + --silent --output /dev/null --show-error --fail +echo "Repository 'customer-service' created." + +curl \ + -f \ + -u "openknowledge:workshop" \ + -d "name=billing-service" \ + -X POST http://gogs-service:3000/api/v1/admin/users/openknowledge/repos \ + --silent --output /dev/null --show-error --fail +echo "Repository 'billing-service' created." + +curl \ + -f \ + -u "openknowledge:workshop" \ + -d "name=delivery-service" \ + -X POST http://gogs-service:3000/api/v1/admin/users/openknowledge/repos \ + --silent --output /dev/null --show-error --fail + +echo "Repository 'delivery-service' created." + +curl \ + -f \ + -u "openknowledge:workshop" \ + -d "name=address-validation-service" \ + -X POST http://gogs-service:3000/api/v1/admin/users/openknowledge/repos \ + --silent --output /dev/null --show-error --fail +echo "Repository 'address-validation-service' created." + +/pushToGogs.sh + +cd /jenkins +./create-jobs.sh From c941c7bc1c52cd30e3fbcca7736d70fcaba2c9e1 Mon Sep 17 00:00:00 2001 From: Nick Wahrenberger Date: Thu, 17 Oct 2024 14:50:25 +0200 Subject: [PATCH 3/3] fixed arm64 issue --- deployment/base/pact/deployment.yaml | 2 +- jenkins/Dockerfile | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/deployment/base/pact/deployment.yaml b/deployment/base/pact/deployment.yaml index 3fe5a6e..559dabb 100644 --- a/deployment/base/pact/deployment.yaml +++ b/deployment/base/pact/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: pact - image: pactfoundation/pact-broker:2.111.0-pactbroker2.107.1 + image: pactfoundation/pact-broker:2.111.0-pactbroker2.107.1-multi ports: - containerPort: 8080 name: http diff --git a/jenkins/Dockerfile b/jenkins/Dockerfile index 411dbd1..de67e03 100644 --- a/jenkins/Dockerfile +++ b/jenkins/Dockerfile @@ -12,7 +12,14 @@ COPY --chown=jenkins:jenkins executors.groovy /usr/share/jenkins/ref/init.groovy # install docker RUN apt-get update -qq && apt-get install --no-install-recommends -qqy apt-transport-https ca-certificates curl gnupg2 software-properties-common\ && curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -\ - && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"\ + && ARCHITECTURE=$(dpkg --print-architecture) \ + && if [ "$ARCHITECTURE" = "amd64" ]; then \ + add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"; \ + elif [ "$ARCHITECTURE" = "arm64" ]; then \ + add-apt-repository "deb [arch=arm64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"; \ + else \ + echo "Unsupported architecture: $ARCHITECTURE" && exit 1; \ + fi \ && apt-get update -qq && apt-get install --no-install-recommends docker-ce -y\ # install kubectl