44
+Not found
+ +Whoops. Looks like this page doesn't exist ¯\_(ツ)_/¯.
+ + ++ +
+diff --git a/cloudbank/404.html b/cloudbank/404.html new file mode 100644 index 000000000..3187de51a --- /dev/null +++ b/cloudbank/404.html @@ -0,0 +1,173 @@ + + +
+ + + + + + + + + + + + + + +Whoops. Looks like this page doesn't exist ¯\_(ツ)_/¯.
+ + ++ +
+Create a service to list all accounts
+Open your AccountsController.java
file and add a final field in the class of type AccountRepository
. And update the constructor to accept an argument of this type and set the field to that value. This tells Spring Boot to inject the JPA repository class we just created into this class. That will make it available to use in our services. The updated parts of your class should look like this:
import com.example.accounts.repository.AccountRepository;
+
+// ...
+
+final AccountRepository accountRepository;
+
+public AccountController(AccountRepository accountRepository) {
+ this.accountRepository = accountRepository;
+}
Now, add a method to get all the accounts from the database and return them. This method should respond to the HTTP GET method. You can use the built-in findAll
method on JpaRepository
to get the data. Your new additions to your class should look like this:
import java.util.List;
+import com.example.accounts.model.Account;
+
+// ...
+
+@GetMapping("/accounts")
+public List<Account> getAllAccounts() {
+ return accountRepository.findAll();
+}
Rebuild and restart your application and test your new endpoint
+If your application is still running, stop it with Ctrl+C (or equivalent) and then rebuild and restart it with this command:
+$ mvn spring-boot:run
This time, when it starts up you will see some new log messages that were not there before. These tell you that it connected to the database successfully.
+2023-02-25 15:58:16.852 INFO 29041 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
+2023-02-25 15:58:16.872 INFO 29041 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.15.Final
+2023-02-25 15:58:16.936 INFO 29041 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
+2023-02-25 15:58:17.658 INFO 29041 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.Oracle12cDialect
+2023-02-25 15:58:17.972 INFO 29041 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
+2023-02-25 15:58:17.977 INFO 29041 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
Now you can test the new service with this command. It will not return any data as we haven’t loaded any data yet.
+$ curl http://localhost:8080/api/v1/accounts
+HTTP/1.1 200
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Sat, 25 Feb 2023 21:00:40 GMT
+
+[]
Add data to ACCOUNTS
table
Notice that Spring Boot automatically set the Content-Type
to application/json
for us. The result is an empty JSON array []
as you might expect. Add some accounts to the database using these SQL statements (run these in your SQLcl terminal):
insert into account.accounts (account_name,account_type,customer_id,account_other_details,account_balance)
+values ('Andy''s checking','CH','abcDe7ged','Account Info',-20);
+insert into account.accounts (account_name,account_type,customer_id,account_other_details,account_balance)
+values ('Mark''s CCard','CC','bkzLp8cozi','Mastercard account',1000);
+commit;
Test the /accounts
service
Now, test the service again. You may want to send the output to jq
if you have it installed, so that it will be formatted for easier reading:
$ curl -s http://localhost:8080/api/v1/accounts | jq .
+[
+ {
+ "accountId": 1,
+ "accountName": "Andy's checking",
+ "accountType": "CH",
+ "accountCustomerId": "abcDe7ged",
+ "accountOpenedDate": "2023-02-26T02:04:54.000+00:00",
+ "accountOtherDetails": "Account Info",
+ "accountBalance": -20
+ },
+ {
+ "accountId": 2,
+ "accountName": "Mark's CCard",
+ "accountType": "CC",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": "2023-02-26T02:04:56.000+00:00",
+ "accountOtherDetails": "Mastercard account",
+ "accountBalance": 1000
+ }
+]
Now that you can query accounts, it is time to create an API endpoint to create an account.
+Create an endpoint to create a new account.
+Now we want to create an endpoint to create a new account. Open AccountController.java
and add a new createAccount
method. This method should return ResponseEntity<Account>
this will allow you to return the account object, but also gives you access to set headers, status code and so on. The method needs to take an Account
as an argument. Add the RequestBody
annotation to the argument to tell Spring Boot that the input data will be in the HTTP request’s body.
Inside the method, you should use the saveAndFlush
method on the JPA Repository to save a new instance of Account
in the database. The saveAndFlush
method returns the created object. If the save was successful, return the created object and set the HTTP Status Code to 201 (Created). If there is an error, set the HTTP Status Code to 500 (Internal Server Error).
Here’s what the new method (and imports) should look like:
+import java.net.URI;
+...
+...
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+// ...
+
+@PostMapping("/account")
+public ResponseEntity<Account> createAccount(@RequestBody Account account) {
+ try {
+ Account newAccount = accountRepository.saveAndFlush(account);
+ URI location = ServletUriComponentsBuilder
+ .fromCurrentRequest()
+ .path("/{id}")
+ .buildAndExpand(newAccount.getAccountId())
+ .toUri();
+ return ResponseEntity.created(location).build();
+ } catch (Exception e) {
+ return new ResponseEntity<>(account, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
Test the /account
endpoint
Rebuild and restart the application as you have previously. Then test the new endpoint. You will need to make an HTTP POST request, and you will need to set the Content-Type
header to application/json
. Pass the data in as JSON in the HTTP request body. Note that Spring Boot Web will handle mapping the JSON to the right fields in the type annotated with the RequestBody
annotation. So a JSON field called accountName
will map to the accountName
field in the JSON, and so on.
Here is an example request and the expected output (yours will be slightly different):
+$ curl -i -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"accountName": "Dave", "accountType": "CH", "accountOtherDetail": "", "accountCustomerId": "abc123xyz"}' \
+ http://localhost:8080/api/v1/account
+HTTP/1.1 201
+Location: http://localhost:8080/api/v1/account/3
+Content-Length: 0
+Date: Wed, 14 Feb 2024 21:33:17 GMT
Notice the HTTP Status Code is 201 (Created). The service returns the URI for the account was created in the header.
+Test endpoint /account
with bad data
Now try a request with bad data that will not be able to be parsed and observe that the HTTP Status Code is 400 (Bad Request). If there happened to be an exception thrown during the save()
method, you would get back a 500 (Internal Server Error):
$ curl -i -X POST -H 'Content-Type: application/json' -d '{"bad": "data"}' http://localhost:8080/api/v1/account
+HTTP/1.1 400
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Sat, 25 Feb 2023 22:05:24 GMT
+Connection: close
+
+{"timestamp":"2023-02-25T22:05:24.350+00:00","status":400,"error":"Bad Request","path":"/api/v1/account"}
Implement Get Account by Account ID endpoint
+Add new method to your AccountController.java
class that responds to the HTTP GET method. This method should accept the account ID as a path variable. To accept a path variable, you place the variable name in braces in the URL path in the @GetMapping
annotation and then reference it in the method’s arguments using the @PathVariable
annotation. This will map it to the annotated method argument. If an account is found, you should return that account and set the HTTP Status Code to 200 (OK). If an account is not found, return an empty body and set the HTTP Status Code to 404 (Not Found).
Here is the code to implement this endpoint:
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import java.util.Optional;
+
+// ...
+
+@GetMapping("/account/{accountId}")
+public ResponseEntity<Account> getAccountById(@PathVariable("accountId") long accountId) {
+ Optional<Account> accountData = accountRepository.findById(accountId);
+ try {
+ return accountData.map(account -> new ResponseEntity<>(account, HttpStatus.OK))
+ .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
Restart and test /account/{accountId}
endpoint
Restart the application and test this new endpoint with this command (note that you created account with ID 2 earlier):
+$ curl -s http://localhost:8080/api/v1/account/2 | jq .
+{
+ "accountId": 2,
+ "accountName": "Mark's CCard",
+ "accountType": "CC",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": "2023-02-26T02:04:56.000+00:00",
+ "accountOtherDetails": "Mastercard account",
+ "accountBalance": 1000
+}
That completes the basic endpoints. In the next task, you can add some additional endpoints if you wish. If you prefer, you can skip that task because you have the option to deploy the fully pre-built service in a later module (Deploy the full CloudBank Application) if you choose.
+Create a project to hold your Account service. In this lab, you will use the Spring Initialzr directly from Visual Studio Code, however it is also possible to use Spring Initialzr online and download a zip file with the generated project.
+Create the project
+In Visual Studio Code, press Ctrl+Shift+P (Cmd+Shift+P on a Mac) to access the command window. Start typing “Spring Init” and you will see a number of options to create a Spring project, as shown in the image below. Select the option to Create a Maven Project.
+Select the Spring Boot Version
+You will be presented with a list of available Spring Boot versions. Choose 3.2.2 (or the latest 3.2.x version available).
+Choose Group ID.
+You will be asked for the Maven Group ID for this new project, you can use com.example (the default value).
+Choose Artifact ID.
+You will be asked for the Maven Artifact ID for this new project, enter account.
+Select Packaging Type
+You will be asked what type of packaging you want for this new project, select JAR from the list of options.
+Choose Java Version
+Next, you will be asked what version of Java to use. Select 21 from the list of options.
+ +Add Spring Boot dependencies
+Now you will have the opportunity to add the Spring Boot dependencies your project needs. For now just add Spring Web, which will let us write some REST services. We will add more later as we need them. After you add Spring Web, click on the option to continue with the selected dependencies.
+After you add Spring Web, click on the option to continue with the selected dependencies.
+ +Select where to save the project
+You will be asked where to save the project. Note that this needs to be an existing location. You may wish to create a directory in another terminal if you do not have a suitable location. Enter the directory to save the project in and press Enter.
+Open the generated project
+Now the Spring Initializr will create a new project based on your selections and place it in the directory you specified. This will only take a few moments to complete. You will a message in the bottom right corner of Visual Studio Code telling you it is complete. Click on the Open button in that message to open your new project in Visual Studio Code.
+Explore the project
+Explore the new project. You should find the main Spring Boot application class and your Spring Boot application.properties
file as shown in the image below.
Remove some files (Optional)
+If desired, you can delete some of the generated files that you will not need. You can remove .mvn
, mvnw
, mvnw.cmd
and HELP.md
if you wish. Leaving them there will not cause any issues.
Build and run the service
+Open a terminal in Visual Studio Code by selecting New Terminal from the Terminal menu (or if you prefer, just use a separate terminal application). Build and run the newly created service with this command:
+$ mvn spring-boot:run
The service will take a few seconds to start, and then you will see some messages similar to these:
+2024-02-14T14:39:52.501-06:00 INFO 83614 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
+2024-02-14T14:39:52.506-06:00 INFO 83614 --- [ main] com.example.account.AccountApplication : Started AccountApplication in 0.696 seconds (process running for 0.833)
Of course, the service does not do anything yet, but you can still make a request and confirm you get a response from it. Open a new terminal and execute the following command:
+$ curl http://localhost:8080
+{"timestamp":"2023-02-25T17:28:23.264+00:00","status":404,"error":"Not Found","path":"/"}
Prepare the data source configuration for deployment
+Update the data source configuration in your src/main/resources/application.yaml
as shown in the example below. This will cause the service to read the correct database details that will be injected into its pod by the Oracle Backend for Spring Boot and Microservices.
datasource:
+ url: ${spring.datasource.url}
+ username: ${spring.datasource.username}
+ password: ${spring.datasource.password}
Add the client and configuration for the Spring Eureka Service Registry
+When you deploy the application to the backend, you want it to register with the Eureka Service Registry so that it can be discovered by other services including the APISIX API Gateway, so that we can easily expose it outside the cluster.
+Add the following line to the <properties>
to the Maven POM file:
<spring-cloud.version>2023.0.0</spring-cloud.version>
Add the dependency for the client to the Maven POM file:
+<dependency>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+</dependency>
Add the dependency management to the Maven POM file:
+<dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-dependencies</artifactId>
+ <version>${spring-cloud.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+</dependencyManagement>
Add the @EnableDiscoveryClient
annotation to the AccountsApplication
class to enable the service registry.
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+// ..
+
+@SpringBootApplication
+@EnableDiscoveryClient
+public class AccountsApplication {
Add the configuration to src/main/resources/application.yaml
file.
eureka:
+ instance:
+ hostname: ${spring.application.name}
+ preferIpAddress: true
+ client:
+ service-url:
+ defaultZone: ${eureka.service-url}
+ fetch-registry: true
+ register-with-eureka: true
+ enabled: true
Build a JAR file for deployment
+Run the following command to build the JAR file (it will also remove any earlier builds). Note that you will need to skip tests now, since you updated the application.yaml
and it no longer points to your local test database instance.
$ mvn clean package -DskipTests
The service is now ready to deploy to the backend.
+Prepare the backend for deployment
+The Oracle Backend for Spring Boot and Microservices admin service is not exposed outside the Kubernetes cluster by default. Oracle recommends using a kubectl port forwarding tunnel to establish a secure connection to the admin service.
+Start a tunnel using this command in a new terminal window:
+$ kubectl -n obaas-admin port-forward svc/obaas-admin 8080
Get the password for the obaas-admin
user. The obaas-admin
user is the equivalent of the admin or root user in the Oracle Backend for Spring Boot and Microservices backend.
$ kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d
Start the Oracle Backend for Spring Boot and Microservices CLI (oractl) in a new terminal window using this command:
+$ oractl
+ _ _ __ _ ___
+/ \ |_) _. _. (_ / | |
+\_/ |_) (_| (_| __) \_ |_ _|_
+========================================================================================
+ Application Name: Oracle Backend Platform :: Command Line Interface
+ Application Version: (1.2.0)
+ :: Spring Boot (v3.3.0) ::
+
+ Ask for help:
+ - Slack: https://oracledevs.slack.com/archives/C03ALDSV272
+ - email: obaas_ww@oracle.com
+
+oractl:>
Connect to the Oracle Backend for Spring Boot and Microservices admin service using the connect
command. Enter obaas-admin
and the username and use the password you collected earlier.
oractl> connect
+username: obaas-admin
+password: **************
+Credentials successfully authenticated! obaas-admin -> welcome to OBaaS CLI.
+oractl:>
Create a database “binding” by tunning this command. Enter the password (Welcome1234##
) when prompted. This will create a Kubernetes secret in the application
namespace called account-db-secrets
which contains the username (account
), password, and URL to connect to the Oracle Autonomous Database instance associated with the Oracle Backend for Spring Boot and Microservices.
oractl:> bind --app-name application --service-name account
+Database/Service Password: *************
+Schema {account} was successfully Not_Modified and Kubernetes Secret {application/account} was successfully Created.
+oractl:>
This created a Kubernetes secret with the credentials to access the database using this Spring Boot microservice application’s username and password. When you deploy the application, its pods will have the keys in this secret injected as environment variables so the application can use them to authenticate to the database.
+Deploy the account service
+You will now deploy your account service to the Oracle Backend for Spring Boot and Microservices using the CLI. You will deploy into the application
namespace, and the service name will be account
. Run this command to deploy your service, make sure you provide the correct path to your JAR file. Note that this command may take 1-3 minutes to complete:
oractl:> deploy --app-name application --service-name account --artifact-path /path/to/accounts-0.0.1-SNAPSHOT.jar --image-version 0.0.1
+uploading: /Users/atael/tmp/cloudbank/accounts/target/accounts-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed
+NOTICE: service not accessible outside K8S
+oractl:>
++What happens when you use the Oracle Backend for Spring Boot and Microservices CLI (oractl) deploy command? When you run the deploy command, the Oracle Backend for Spring Boot and Microservices CLI does several things for you:
+
Verify account service
+You can check if the account service is running properly by running the following command:
+$ kubectl logs -n application svc/account
The command will return the logfile content for the account service. If everything is running properly you should see something like this:
+2023-06-01 20:44:24.882 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
+2023-06-01 20:44:24.883 INFO 1 --- [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080
+2023-06-01 20:44:24.903 INFO 1 --- [ main] c.example.accounts.AccountsApplication : Started AccountsApplication in 14.6 seconds (JVM running for 15.713)
+2023-06-01 20:44:31.971 INFO 1 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2023-06-01 20:44:31.971 INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
+2023-06-01 20:44:31.975 INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
Check the Eureka Server
+Create a tunnel to the Eureka server, so you can verify the ACCOUNTS
application has registered with the server.
$ kubectl -n eureka port-forward svc/eureka 8761
Open a web browser to Eureka Dashboard to vew the Eureka Server dashboard web user interface. It will look similar to this. Note that the ACCOUNT
application you have built has registered with Eureka.
Now that the account service is deployed, you need to expose it through the API Gateway so that clients will be able to access it. This is done by creating a “route” in APISIX Dashboard.
+Retrieve the admin password for the APISIX API Gateway.
+Execute the following command to get the password for the admin
user for the APISIX API Gateway:
$ kubectl get secret -n apisix apisix-dashboard -o jsonpath='{.data.conf\.yaml}' | base64 -d | grep 'password:'
Access the APISIX Dashboard
+The APISIX Dashboard isn’t exposed outside the cluster. You need to start a tunnel to be able to access APISIX Dashboard. Start the tunnel using this command in a new terminal window:
+$ kubectl -n apisix port-forward svc/apisix-dashboard 8090:80
Open a web browser to APISIX Dashboard to view the APISIX Dashboard web user interface. It will appear similar to the image below.
+If prompted to login, login with username admin
and the password you retrieved earlier. Note that Oracle strongly recommends that you change the password, even though this interface is not accessible outside the cluster without a tunnel.
Open the routes page from the left hand side menu. You will not have any routes yet.
+ +Create the route
+Click on the Create button to start creating a route. The Create route page will appear. Enter account
in the Name field:
Scroll down to the Request Basic Define section. Set the Path to /api/v1/account*
. This tells APISIX API Gateway that any incoming request for that URL path (on any host or just IP address) should use this route. In the HTTP Method select GET
, POST
, DELETE
, and OPTIONS
. The first three you will recall using directly in the implementation of the account service during this lab. User interfaces and other clients will often send an OPTIONS
request before a “real” request to see if the service exists and check headers and so on, so it is a good practice to allow OPTIONS
as well.
Click on the Next button to move to the Define API Backend Server page. On this page you configure where to route requests to. In the Upstream Type field, select Service Discovery. Then in the Discovery Type field, select Eureka. In the Service Name field enter ACCOUNT
. This tells APISIX to lookup the service in Spring Eureka Service Registry with the key ACCOUNT
and route requests to that service using a Round Robin algorithm to distribute requests.
Click on Next to go to the Plugin Config page. You will not add any plugins right now. You may wish to browse through the list of available plugins on this page. When you are ready, click on Next to go to the Preview page. Check the details and then click on Submit to create the route.
+When you return to the route list page, you will see your new account
route in the list now.
Verify the account service
+In the next two commands, you need to provide the correct IP address for the API Gateway in your backend environment. You can find the IP address using this command, you need the one listed in the EXTERNAL-IP
column:
$ kubectl -n ingress-nginx get service ingress-nginx-controller
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ingress-nginx-controller LoadBalancer 10.123.10.127 100.20.30.40 80:30389/TCP,443:30458/TCP 13d
Test the create account endpoint with this command, use the IP address (EXTERNAL-IP in the table above) for your API Gateway:
+$ curl -i -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"accountName": "Sanjay''s Savings", "accountType": "SA", "accountCustomerId": "bkzLp8cozi", "accountOtherDetails": "Savings Account"}' \
+ http://<EXTERNAL-IP>/api/v1/account
+HTTP/1.1 201
+Date: Wed, 01 Mar 2023 18:35:31 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"accountId":24,"accountName":"Sanjays Savings","accountType":"SA","accountCustomerId":"bkzLp8cozi","accountOpenedDate":null,"accountOtherDetails":"Savings Account","accountBalance":0}
Test the get account endpoint with this command, use the IP address for your API Gateway and the accountId
that was returned in the previous command:
$ curl -s http://<EXTERNAL-IP>/api/v1/account/24 | jq .
+{
+ "accountId": 24,
+ "accountName": "Sanjay's Savings",
+ "accountType": "SA",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": null,
+ "accountOtherDetails": "Savings Account",
+ "accountBalance": 1040
+}
Your service is deployed in the Oracle Backend for Spring Boot and Microservices environment and using the Oracle Autonomous Database instance associated with the backend.
+Now that the account service is deployed, you need to expose it through the API Gateway so that clients will be able to access it. This is done by creating a “route” in APISIX Dashboard.
+Retrieve the admin password for the APISIX API Gateway.
+Execute the following command to get the password for the admin
user for the APISIX API Gateway:
$ kubectl get secret -n apisix apisix-dashboard -o jsonpath='{.data.conf\.yaml}' | base64 -d | grep 'password:'
Access the APISIX Dashboard
+The APISIX Dashboard isn’t exposed outside the cluster. You need to start a tunnel to be able to access APISIX Dashboard. Start the tunnel using this command in a new terminal window:
+$ kubectl -n apisix port-forward svc/apisix-dashboard 8090:80
Open a web browser to APISIX Dashboard to view the APISIX Dashboard web user interface. It will appear similar to the image below.
+If prompted to login, login with username admin
and the password you retrieved earlier. Note that Oracle strongly recommends that you change the password, even though this interface is not accessible outside the cluster without a tunnel.
Open the routes page from the left hand side menu. You will not have any routes yet.
+ +Create the route
+Click on the Create button to start creating a route. The Create route page will appear. Enter account
in the Name field:
Scroll down to the Request Basic Define section. Set the Path to /api/v1/account*
. This tells APISIX API Gateway that any incoming request for that URL path (on any host or just IP address) should use this route. In the HTTP Method select GET
, POST
, DELETE
, and OPTIONS
. The first three you will recall using directly in the implementation of the account service during this lab. User interfaces and other clients will often send an OPTIONS
request before a “real” request to see if the service exists and check headers and so on, so it is a good practice to allow OPTIONS
as well.
Click on the Next button to move to the Define API Backend Server page. On this page you configure where to route requests to. In the Upstream Type field, select Service Discovery. Then in the Discovery Type field, select Eureka. In the Service Name field enter ACCOUNT
. This tells APISIX to lookup the service in Spring Eureka Service Registry with the key ACCOUNT
and route requests to that service using a Round Robin algorithm to distribute requests.
Click on Next to go to the Plugin Config page. You will not add any plugins right now. You may wish to browse through the list of available plugins on this page. When you are ready, click on Next to go to the Preview page. Check the details and then click on Submit to create the route.
+When you return to the route list page, you will see your new account
route in the list now.
Verify the account service
+In the next two commands, you need to provide the correct IP address for the API Gateway in your backend environment. You can find the IP address using this command, you need the one listed in the EXTERNAL-IP
column:
$ kubectl -n ingress-nginx get service ingress-nginx-controller
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ingress-nginx-controller LoadBalancer 10.123.10.127 100.20.30.40 80:30389/TCP,443:30458/TCP 13d
Test the create account endpoint with this command, use the IP address (EXTERNAL-IP in the table above) for your API Gateway:
+$ curl -i -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"accountName": "Sanjay''s Savings", "accountType": "SA", "accountCustomerId": "bkzLp8cozi", "accountOtherDetails": "Savings Account"}' \
+ http://<EXTERNAL-IP>/api/v1/account
+HTTP/1.1 201
+Date: Wed, 01 Mar 2023 18:35:31 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"accountId":24,"accountName":"Sanjays Savings","accountType":"SA","accountCustomerId":"bkzLp8cozi","accountOpenedDate":null,"accountOtherDetails":"Savings Account","accountBalance":0}
Test the get account endpoint with this command, use the IP address for your API Gateway and the accountId
that was returned in the previous command:
$ curl -s http://<EXTERNAL-IP>/api/v1/account/24 | jq .
+{
+ "accountId": 24,
+ "accountName": "Sanjay's Savings",
+ "accountType": "SA",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": null,
+ "accountOtherDetails": "Savings Account",
+ "accountBalance": 1040
+}
Your service is deployed in the Oracle Backend for Spring Boot and Microservices environment and using the Oracle Autonomous Database instance associated with the backend.
+If you would like to learn more about endpoints and implement the remainder of the account-related endpoints, this task provides the necessary details.
+Implement Get Accounts for Customer ID endpoint
+Add a new method to your AccountController.java
class that responds to the HTTP GET method. This method should accept a customer ID as a path variable and return a list of accounts for that customer ID. If no accounts are found, return an empty body and set the HTTP Status Code to 204 (No Content).
Here is the code to implement this endpoint:
+import java.util.ArrayList;
+
+// ...
+
+@GetMapping("/account/getAccounts/{customerId}")
+public ResponseEntity<List<Account>> getAccountsByCustomerId(@PathVariable("customerId") String customerId) {
+ try {
+ List<Account> accountData = new ArrayList<Account>();
+ accountData.addAll(accountRepository.findByAccountCustomerId(customerId));
+ if (accountData.isEmpty()) {
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ }
+ return new ResponseEntity<>(accountData, HttpStatus.OK);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
You will also need to update your AccountRepository.java
class to add the extra find method you need for this endpoint.
import java.util.List;
+
+// ...
+
+public interface AccountRepository extends JpaRepository <Account, Long> {
+ List<Account> findByAccountCustomerId(String customerId);
+}
Test the /account/getAccounts/{customerId}
endpoint
Restart the application and test the new endpoint with this command (note that you created this account and customer ID earlier):
+$ curl -s http://localhost:8080/api/v1/account/getAccounts/bkzLp8cozi | jq .
+[
+ {
+ "accountId": 2,
+ "accountName": "Mark's CCard",
+ "accountType": "CC",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": "2023-02-26T02:04:56.000+00:00",
+ "accountOtherDetails": "Mastercard account",
+ "accountBalance": 1000
+ }
+]
Implement a Delete Account API endpoint
+Add a new method to your AccountController.java
file that responds to the HTTP DELETE method and accepts an account ID as a path variable. You can use the @DeleteMapping
annotation to respond to HTTP DELETE. This method should delete the account specified and return an empty body and HTTP Status Code 204 (No Content) which is generally accepted to mean the deletion was successful (some people also use 200 (OK) for this purpose).
Here is the code to implement this endpoint:
+import org.springframework.web.bind.annotation.DeleteMapping;
+
+// ...
+
+@DeleteMapping("/account/{accountId}")
+public ResponseEntity<HttpStatus> deleteAccount(@PathVariable("accountId") long accountId) {
+ try {
+ accountRepository.deleteById(accountId);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ } catch (Exception e) {
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
Test the Delete /account/{accountId}
endpoint
Restart the application and test this new endpoint by creating and deleting an account. First create an account:
+$ curl -i -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"accountName": "Bob", "accountType": "CH", "accountOtherDetail": "", "accountCustomerId": "bob808bob"}' \
+ http://localhost:8080/api/v1/account
+HTTP/1.1 201
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Wed, 01 Mar 2023 13:23:44 GMT
+
+{"accountId":42,"accountName":"Bob","accountType":"CH","accountCustomerId":"bob808bob","accountOpenedDate":"2023-03-01T18:23:44.000+00:00","accountOtherDetails":null,"accountBalance":0}
Verify that account exists:
+$ curl -s http://localhost:8080/api/v1/account/getAccounts/bob808bob | jq .
+[
+ {
+ "accountId": 42,
+ "accountName": "Bob",
+ "accountType": "CH",
+ "accountCustomerId": "bob808bob",
+ "accountOpenedDate": "2023-03-01T18:23:44.000+00:00",
+ "accountOtherDetails": null,
+ "accountBalance": 0
+ }
+]
Delete the account. Note that your account ID may be different, check the output from the previous command to get the right ID and replace 42
at the end of the URL with your ID:
$ curl -i -X DELETE http://localhost:8080/api/v1/account/42
+HTTP/1.1 204
+Date: Wed, 01 Mar 2023 13:23:56 GMT
Verify the account no longer exists:
+$ curl -s http://localhost:8080/api/v1/account/getAccounts/bob808bob | jq .
That completes the account endpoints. Now it is time to deploy your service to the backend.
+Implement the first simple endpoint
+AccountController.java
Create a new directory in the directory src/main/java/com/example/accounts
called controller
. In that new directory, create a new Java file called AccountController.java
. When prompted for the type, choose class.
Your new file should look like this:
+package com.example.accounts.controller;
+
+public class AccountController {
+
+}
RestController
annotationAdd the RestController
annotation to this class to tell Spring Boot that we want this class to expose REST services. You can just start typing @RestController
before the public class
statement and Visual Studio Code will offer code completion for you. When you select from the pop-up, Visual Studio Code will also add the import statement for you. The list of suggestions is based on the dependencies you added to your project.
Add the RequestMapping
annotation to this class as well, and set the URL path to /api/v1
. Your class should now look like this:
package com.example.accounts.controller;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1")
+public class AccountController {
+
+}
ping
methodAdd a method to this class called ping
which returns a String
with a helpful message. Add the GetMapping
annotation to this method and set the URL path to /hello
. Your class should now look like this:
package com.example.accounts.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1")
+public class AccountController {
+
+ @GetMapping("/hello")
+ public String ping() {
+ return "Hello from Spring Boot";
+ }
+
+}
You have just implemented your first REST service in Spring Boot! This service will be available on http://localhost:8080/api/v1/hello
. And the GetMapping
annotation tells Spring Boot that this service will respond to the HTTP GET method.
You can test your service now by building and running again. Make sure you save the file. If you still have the application running from before, hit Ctrl+C (or equivalent) to stop it, and then build and run with this command:
+$ mvn spring-boot:run
Then try to call your service with this command:
+$ curl -i http://localhost:8080/api/v1/hello
+HTTP/1.1 200
+Content-Type: text/plain;charset=UTF-8
+Content-Length: 22
+Date: Sat, 25 Feb 2023 17:59:52 GMT
+
+Hello from Spring Boot
Great, it works! Notice it returned HTTP Status Code 200 (OK) and some HTTP Headers along with the body which contained your message. Later we will see how to return JSON and to set the status code appropriately.
+This is a new chapter.
+ + +This module walks you through the steps to build a Spring Boot microservice from scratch, and to deploy it into the Oracle Backend for SpringBoot and Microservices. In this lab, we will build the “Account” microservice. In the next lab, the remaining Cloud Bank microservices will be provided for you.
+Estimated Time: 20 minutes
+Quick walk through on how to build an account microservice.
+ +In this lab, you will:
+This module assumes you have:
+Spring Data JPA allows our Spring Boot application to easily use the database. It uses simple Java POJOs to represent the data model and provides a lot of out-of-the-box features which means there is a lot less boilerplate code to be written.
+To add Spring Data JPA and the Oracle Database drivers to your project, open the Maven POM (pom.xml
) and add these extra dependencies for Spring Data JPA, Oracle Spring Boot Starters for Oracle Database UCP (Universal Connection Pool) and Wallet:
```xml
+
+<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
+</dependency>
+<dependency>
+ <groupId>com.oracle.database.spring</groupId>
+ <artifactId>oracle-spring-boot-starter-ucp</artifactId>
+ <version>23.4.0</version>
+</dependency>
+ <dependency>
+ <groupId>com.oracle.database.spring</groupId>
+ <artifactId>oracle-spring-boot-starter-wallet</artifactId>
+ <version>23.4.0</version>
+</dependency>
+```
+
+Visual Studio code will display a notification in the bottom right corner and ask if it should update the project based on the change you just made. You should select Yes or Always to this notification. Doing so will ensure that the auto-completion will have access to the classes in the new dependency that you just added.
+ +Configure JPA Datasource
+To configure Spring Data JPA access to the database, you will add some configuration information to the Spring Boot application properties (or YAML) file. Access to the database you need to unzip the Wallet and get information from those files.
+$ unzip /path/to/wallet/wallet_name.zip
sqlnet.ora
file so that the section (DIRECTORY="?/network/admin")
matches the full path to the directory where you unzipped the Wallet, for example:WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="/path/to/unzipped/wallet")))
cbankdb_tp
.$ grep "_tp =" /path/to/unzipped/wallet/tnsnames.ora | cut -d"=" -f 1
+cbankdb_tp
You will find a file called application.properties
in the src/main/resources
directory in your project. You can use either properties format or YAML format for this file. In this lab, you will use YAML. Rename the file to application.yaml
and then add this content to the file. Make sure that you modify the url to contain the path to the wallet and the name of the TNS entry you collected earlier.
spring:
+ application:
+ name: account
+ jpa:
+ hibernate:
+ ddl-auto: validate
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.OracleDialect
+ format_sql: true
+ show-sql: true
+ datasource:
+ url: jdbc:oracle:thin:@tns_entry_from_above?TNS_ADMIN=/path/to/Wallet
+ username: account
+ password: Welcome1234##
+ driver-class-name: oracle.jdbc.OracleDriver
+ type: oracle.ucp.jdbc.PoolDataSource
+ oracleucp:
+ connection-factory-class-name: oracle.jdbc.pool.OracleDataSource
+ connection-pool-name: AccountConnectionPool
+ initial-pool-size: 15
+ min-pool-size: 10
+ max-pool-size: 30
+ ```
+
+These parameters will be used by Spring Data JPA to automatically configure the data source and inject it into your application. This configuration uses [Oracle Universal Connection Pool](https://docs.oracle.com/en/database/oracle/oracle-database/21/jjucp/index.html) to improve performance and better utilize system resources. The settings in the `spring.jpa` section tell Spring Data JPA to use Oracle SQL syntax, and to show the SQL statements in the log, which is useful during development when you may wish to see what statements are being executed as your endpoints are called.
Create the data model in the Spring Boot application
+Create a new directory inside src/main/java/com/example/accounts
called model
and inside that new directory, create a new Java file called Account.java
, when prompted for a type, choose class.
In this class you can define the fields that will make up the “account” object, as shown below. Also add a constructor for the non-generated fields.
+package com.example.accounts.model;
+
+import java.util.Date;
+
+public class Account {
+
+ private long accountId;
+ private String accountName;
+ private String accountType;
+ private String accountCustomerId;
+ private Date accountOpenedDate;
+ private String accountOtherDetails;
+ private long accountBalance;
+
+ public Account(String accountName, String accountType, String accountOtherDetails, String accountCustomerId) {
+ this.accountName = accountName;
+ this.accountType = accountType;
+ this.accountOtherDetails = accountOtherDetails;
+ this.accountCustomerId = accountCustomerId;
+ }
+}
Now, you need to give Spring Data JPA some hints about how to map these fields to the underlying database objects. Spring Data JPA can actually automate creation of database objects for you, and that can be very helpful during development and testing. But in many real-world cases, the database objects will already exist, so in this module you will work with pre-existing database objects.
+Before continuing, open the Maven POM (pom.xml
) for the project and add this new dependency to the list. Lombok offers various annotations aimed at replacing Java code that is well known for being boilerplate, repetitive, or tedious to write. You’ll use it to avoid writing getters, setters, constructors and builders.
<dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+</dependency>
Visual Studio code will display a notification in the bottom right corner and ask if it should update the project based on the change you just made. You should select Yes or Always to this notification. Doing so will ensure that the auto-completion will have access to the classes in the new dependency that you just added.
+ +Add the Data
and NoArgsConstructor
Lombok annotations to your Account
class. @Data
generates all the boilerplate that is normally associated with simple POJOs and beans: getters for all fields, setters for all non-final fields, and appropriate toString
, equals
and hashCode
implementations that involve the fields of the class, and a constructor that initializes all final fields, as well as all non-final fields with no initializer that have been marked with @NonNull
, in order to ensure the field is never null. The NoArgsConstructor
creates a constructor with no arguments.
Also add the JPA Entity
and Table
annotations to the class and set the Table
’s name
property to accounts
. These tell JPA that this object will be mapped to a table in the database called accounts
. Your class should now look like this:
package com.example.accounts.model;
+
+import java.util.Date;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@Entity
+@Table(name = "ACCOUNTS")
+public class Account {
+ // ...
+}
You also need to give some hints about the columns in the existing tables. You should add a Column
annotation to each field and set its name
property to the name of the database column. Some of the columns will need additional information.
First, the accountId
field is the primary key, so add the Id
annotation to it, and its value is generated, so add the GeneratedValue
annotation and set its strategy
property to GenerationType.IDENTITY
.
Next, the accountOpenedDate
field is special - it should not be able to be inserted or updated. So you will add the updatable
and insertable
properties to its Column
annotation and set them both to false
. Also add the Generated
annotation and set it to GenerationTime.INSERT
to tell Spring Data JPA that the value for this field should be generated at the time of the database insert operation.
With these additions, the fields in your class should now look like this, the extra imports are also shown:
+import org.hibernate.annotations.Generated;
+import org.hibernate.annotations.GenerationTime;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+// ...
+
+@Id
+@GeneratedValue(strategy = GenerationType.IDENTITY)
+@Column(name = "ACCOUNT_ID")
+private long accountId;
+
+@Column(name = "ACCOUNT_NAME")
+private String accountName;
+
+@Column(name = "ACCOUNT_TYPE")
+private String accountType;
+
+@Column(name = "CUSTOMER_ID")
+private String accountCustomerId;
+
+@SuppressWarnings("deprecation")
+@Generated(GenerationTime.INSERT)
+@Column(name = "ACCOUNT_OPENED_DATE", updatable = false, insertable = false)
+private Date accountOpenedDate;
+
+@Column(name = "ACCOUNT_OTHER_DETAILS")
+private String accountOtherDetails;
+
+@Column(name = "ACCOUNT_BALANCE")
+private long accountBalance;
Create the JPA Repository definition
+Create a new directory in src/main/java/com/example/accounts
called repository
and in the new directory, create a new Java file called AccountRepository.java
. When prompted for the type, choose interface. Update the interface definition to extend JpaRepository
with type parameters <Account, Long>
. Account
is the model class you just created, and Long
is the type of the primary key. Your interface should look like this:
package com.example.accounts.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import com.example.account.model.Account;
+
+public interface AccountRepository extends JpaRepository<Account, Long> {
+}
By extending JpaRepository
you will get a lot of convenient methods “for free”. You will use one of them now to create an endpoint to list all accounts.
Get the database user ADMIN
password
The ADMIN password can be retrieved from a k8s secret using this command. Replace the DBNAME with the name of your database. Save the password as it will be needed in later steps.
+$ kubectl -n application get secret DBNAME-db-secrets -o jsonpath='{.data.db\.password}' | base64 -d
If you don’t know the name of the database, execute the following command and look for the line DBNAME-db-secrets.
+$ kubectl -n application get secrets
Start SQLcl and load the Wallet
+The Accounts service is going to have two main objects - an account
and a journal
. Here are the necessary steps to create the objects in the database
If you installed SQLcl as recommended, you can connect to your database using this SQLcl (or use the SQLcl session created during module two, Setup). Start SQLcl in a new terminal window.
+$ sql /nolog
+
+SQLcl: Release 22.4 Production on Fri Mar 03 12:25:24 2023
+
+Copyright (c) 1982, 2023, Oracle. All rights reserved.
+
+SQL>
Load the Wallet
+When you are connected, run the following command to load the Wallet you downloaded during the Setup lab. Replace the name of the waller and location of the Wallet to match your environment.
+SQL> set cloudconfig /path/to/wallet/wallet-name.zip
Connect to the Database
+If you need to see what TNS Entries you have run the show tns
command. For example:
SQL> show tns
+CLOUD CONFIG set to: /Users/atael/tmp/wallet/Wallet_CBANKDB.zip
+
+TNS Lookup Locations
+--------------------
+
+TNS Locations Used
+------------------
+1. /Users/atael/tmp/wallet/Wallet_CBANKDB.zip
+2. /Users/atael
+
+Available TNS Entries
+---------------------
+CBANKDB_HIGH
+CBANKDB_LOW
+CBANKDB_MEDIUM
+CBANKDB_TP
+CBANKDB_TPURGENT
Connect to the database using the ADMIN
user, the password you retrieved earlier and the TNS name DBNAME_tp
.
SQL> connect ADMIN/your-ADMIN-password@your-TNS-entry
+Connected.
Create Database Objects
+Run the SQL statements below to create the database objects:
+-- create a database user for the account service
+create user account identified by "Welcome1234##";
+
+-- add roles and quota
+grant connect to account;
+grant resource to account;
+alter user account default role connect, resource;
+alter user account quota unlimited on users;
+
+-- create accounts table
+create table account.accounts (
+ account_id number generated always as identity (start with 1 cache 20),
+ account_name varchar2(40) not null,
+ account_type varchar2(2) check (account_type in ('CH', 'SA', 'CC', 'LO')),
+ customer_id varchar2 (20),
+ account_opened_date date default sysdate not null,
+ account_other_details varchar2(4000),
+ account_balance number
+) logging;
+
+alter table account.accounts add constraint accounts_pk primary key (account_id) using index logging;
+
+comment on table account.accounts is 'CloudBank accounts table';
+
+-- create journal table
+create table account.journal (
+ journal_id number generated always as identity (start with 1 cache 20),
+ journal_type varchar2(20),
+ account_id number,
+ lra_id varchar2(1024) not null,
+ lra_state varchar2(40),
+ journal_amount number
+) logging;
+
+alter table account.journal add constraint journal_pk primary key (journal_id) using index logging;
+
+comment on table account.journal is 'CloudBank accounts journal table';
+/
Now that the database objects are created, you can configure Spring Data JPA to use them in your microservice.
+ + +Oracle Backend for Spring Boot and Microservices includes Spring Admin which provides a web user interface for managing and monitoring Spring applications.
+Connect to Spring Admin
+Oracle Backend for Spring Boot and Microservices does not expose management interfaces outside the Kubernetes cluster for improved security. Oracle recommends you access these interfaces using kubectl port forwarding, which creates an encrypted tunnel from your client machine to the cluster to access a specific service in the cluster.
+Open a tunnel to the Spring Admin server using this command:
+kubectl -n admin-server port-forward svc/admin-server 8989
Open a web browser to http://localhost:8989 to view the Spring Admin web user interface.
+Click on the Wallboard link in the top menu to view the “wallboard” which shows all the discovered services. Spring Admin discovers services from the Spring Eureka Service Registry.
+ +Each hexagon represents a service. Notice that this display gives you a quick overview of the health of your system. Green services are healthy, grey services have reduced availability and red services are not healthy. You can also see information about how many instances (i.e. pods) are available for each service.
+View information about a service
+Click on the Customer service. You will see a detail page like this:
+ +On this page, you can see detailed information about service’s health, and you can scroll down to see information about resource usage. The menu on the left hand side lets you view additional information about the service including its environment variables, the Spring beans loaded, its Spring configuration properties and so on. You can also access metrics from this interface.
+View endpoints
+Click on the Mappings link on the left hand side menu. This page shows you information about the URL Path mappings (or endpoints) exposed by this service. You will notice several endpoints exposed by Spring Actuator, which enables this management and monitoring to be possible. And you will see your service’s own endpoints, in this example the ones that start with /api/v1/...
:
Oracle Backend for Spring Boot and Microservices includes APISIX API Gateway to manage which services are made available outside the Kubernetes cluster. APISIX allows you to manage many aspects of the services’ APIs including authentication, logging, which HTTP methods are accepted, what URL paths are exposed, and also includes capabilities like rewriting, filtering, traffic management and has a rich plugin ecosystem to enhance it with additional capabilities. You can manage the APISIX API Gateway using the APISIX Dashboard.
+Access the APISIX Dashboard
+Start the tunnel using this command. You can run this in the background if you prefer.
+$ kubectl -n apisix port-forward svc/apisix-dashboard 8081:80
Open a web browser to http://localhost:8081 to view the APISIX Dashboard web user interface. It will appear similar to the image below.
+If prompted to login, login with username admin
and password admin
. Note that Oracle strongly recommends that you change the password, even though this interface is not accessible outside the cluster without a tunnel.
Open the routes page from the left hand side menu. You will see the routes that you defined in earlier modules:
+ +View details of a route
+Click on the Configure button next to the account route. The first page shows information about the route definition. Scroll down to the Request Basic Define section. Notice how you can set the host, port, paths, HTTP Methods and other information for the API.
+ +Click on the Next button to move to the Define API Backend Server page where you can set the routing/load balancing algorithm, retries, timeout and so on. On this page you will notice that the upstream service is defined using Service Discovery and the discovery type is Eureka. The Service Name specified here is the key used to look up the service in the Spring Eureka Service Registry. APISIX will route to any available instance of the service registered in Eureka.
+ +Click on the Next button to move to the Plugin Config page. The routes in the CloudBank sample do not use any of the plugins, however you can scroll through this page to get an idea of what plugins are available for your services.
+ +++Note: You can find detailed information about the available plugins and how to configure them in the APISIX documentation in the Plugins section.
+
The Spring Config Server can be used to store configuration information for Spring Boot applications, so that the configuration can be injected at runtime. It organized the configuration into properties, which are essentially key/value pairs. Each property can be assigned to an application, a moduleel, and a profile. This allows a running application to be configured based on metadata which it will send to the Spring Config Server to obtain the right configuration data.
+The configuration data is stored in a table in the Oracle Autonomous Database instance associated with the backend.
+Look at the configuration data
+Execute the query below by pasting it into the SQL worksheet in Database Actions (which you learned how to open in Task 2 above) and clicking on the green circle “play” icon. This query shows the externalized configuration data stored by the Spring Config Server.
+select * from configserver.properties
In this example you can see there is an application called fraud
, which has two configuration properties for the profile kube
and moduleel latest
.
Oracle Backend for Spring Boot and Microservices includes an Oracle Autonomous Database instance. You can manage and access the database from the OCI Console.
+View details of the Oracle Autonomous Database
+In the OCI Console, in the main (“hamburger”) menu navigate to the Oracle Database category and then Oracle Autonomous Database. Make sure you have the correct region selected (in the top right corner) and the compartment where you installed Oracle Backend for Spring Boot and Microservices (on the left hand side pull down list). You will a list of Oracle Autonomous Database instances (you will probably only have one):
+ +Click on the database name link to view more information about that instance. ON this page, you can see important information about your Oracle Autonomous Database instance, and you can manage backups, access and so on. You can also click on the Performance Hub button to access information about the performance of your database instance.
+ +You can manage scaling from here by clicking on the Manage resource allocation button which will open this form where you can adjust the ECPU and storage for the Autonomous Database instance.
+ +Explore Oracle Backend for Spring Boot and Microservices database objects
+Click on the Database Actions button and select SQL to open a SQL Worksheet.
+ +Depending on choices you made during installation, you may go straight to SQL Worksheet, or you may need to enter credentials first. If you are prompted to login, use the username ADMIN
and obtain the password from Kubernetes with this command (make sure to change the secret name to match the name you chose during installation):
$ kubectl -n application get secret obaasdevdb-db-secrets -o jsonpath='{.data.db\.password}' | base64 -d
In the SQL Worksheet, you can the first pull down list in the Navigator on the left hand side to see the users and schema in the database. Choose the CONFIGSERVER user to view tables (or other objects) for that user. This is the user associated with the Spring Config Server.
+Execute this query to view tables associated with various Spring Boot services and the CloudBank:
+select owner, table_name
+from dba_tables
+where owner in ('ACCOUNT', 'CUSTOMER', 'CONFIGSERVER', 'AZNSERVER')
Feel free to explore some of these tables to see the data.
+Spring Eureka Service Registry is an application that holds information about what microservices are running in your environment, how many instances of each are running, and on which addresses and ports. Spring Boot microservices register with Eureka at startup, and it regularly checks the health of all registered services. Services can use Eureka to make calls to other services, thereby eliminating the need to hard code service addresses into other services.
+Start a port-forward tunnel to access the Eureka web user interface
+Start the tunnel using this command. You can run this in the background if you prefer.
+$ kubectl -n eureka port-forward svc/eureka 8761:8761
Open a web browser to http://localhost:8761 to view the Eureka web user interface. It will appear similar to the image below.
+ +Notice that you can see your own services like the Accounts, Credit Score and Customer services from the CloudBank sample application, as well as platform services like Spring Admin, the Spring Config server and Conductor.
+Grafana provides an easy way to access the metrics collected in the backend and to view them in dashboards. It can be used to monitor performance, as well as to identify and analyze problems and to create alerts.
+Explore the pre-installed Spring Boot Dashboard
+Get the password for the Grafana admin user using this command (your output will be different):
+$ kubectl -n grafana get secret grafana -o jsonpath='{.data.admin-password}' | base64 -d
+fusHDM7xdwJXyUM2bLmydmN1V6b3IyPVRUxDtqu7
Start the tunnel using this command. You can run this in the background if you prefer.
+$ kubectl -n grafana port-forward svc/grafana 8080:80
Open a web browser to http://localhost:8080/grafana/ to view the Grafana web user interface. It will appear similar to the image below. Log in with the username admin and the password you just got.
+ +After signing in you will get to the Grafana homepage.
+ +On the left, click on Dashboards to set the list of pre-installed dashboards.
+ +click on the link to Spring Boot 3.x Statistics to see Spring Boot information.
+The Spring Boot Dashboard looks like the image below. Use the Instance selector at the top to choose which microservice you wish to view information for.
+ +Feel free to explore the other dashboards that are preinstalled.
+This is a new chapter.
+ + +This module walks you through various features of Oracle Backend for Spring Boot and Microservices, and shows you how to use them.
+Estimated Time: 20 minutes
+Quick walk through on how to explore backend platform.
+ +In this module, you will:
+This module assumes you have:
+Jaeger provides a way to view the distributed tracing information that is automatically collected by the backend. This allows you to follow requests from the entry point of the platform (the API Gateway) through any number of microservices, including database and messaging operations those services may perform.
+View a trace
+Start the tunnel using this command. You can run this in the background if you prefer.
+$ kubectl -n observability port-forward svc/jaegertracing-query 16686
Open a web browser to http://localhost:16686 to view the Jaeger web user interface. It will appear similar to the image below.
+ +Select one of the Cloudbank services for example account
and an operation for example http get /api/v1/account/{account}
. Click on the Find traces button to find a trace, open any one and explore the details.
Click on one of the traces to explore the trace.
+ +Oracle Backend for Spring Boot and Microservices includes a number of platform services which are deployed into the Oracle Container Engine for Kubernetes cluster. You configured kubectl to access your cluster in an earlier module. In this task, you will explore the services deployed in the Kubernetes cluster. A detailed explanation of Kubernetes concepts is beyond the scope of this course.
+Explore namespaces
+Kubernetes resources are grouped into namespaces. To see a list of the namespaces in your cluster, use this command, your output will be slightly different:
+$ kubectl get ns
+ NAME STATUS AGE
+ admin-server Active 4h56m
+ apisix Active 4h56m
+ application Active 4h57m
+ azn-server Active 4h56m
+ cert-manager Active 4h59m
+ coherence Active 4h56m
+ conductor-server Active 4h56m
+ config-server Active 4h55m
+ default Active 5h8m
+ eureka Active 4h57m
+ grafana Active 4h55m
+ ingress-nginx Active 4h57m
+ kafka Active 4h56m
+ kaniko Active 5h1m
+ kube-node-lease Active 5h8m
+ kube-public Active 5h8m
+ kube-state-metrics Active 4h57m
+ kube-system Active 5h8m
+ metrics-server Active 4h57m
+ obaas-admin Active 4h55m
+ observability Active 4h55m
+ open-telemetry Active 4h55m
+ oracle-database-exporter Active 4h55m
+ oracle-database-operator-system Active 4h59m
+ otmm Active 4h55m
+ prometheus Active 4h57m
+ vault Active 4h54m
Here is a summary of what is in each of these namespaces:
+admin-server
contains Spring Admin which can be used to monitor and manage your servicesapisix
contains the APISIX API Gateway and Dashboard which can be used to expose services outside the clusterapplication
is a pre-created namespace with the Oracle Database wallet and secrets pre-configured to allow services deployed there to access the Oracle Autonomous Database instancecert-manager
contains Cert Manager which is used to manage X.509 certificates for servicescloudbank
is the namespace where you deployed the CloudBank sample applicationconductor-server
contains Netflix Conductor OSS which can be used to manage workflowsconfig-server
contains the Spring CLoud Config Servereureka
contains the Spring Eureka Service Registry which is used for service discoverygrafana
contains Grafana which can be used to monitor and manage your environmentingress-nginx
contains the NGINX ingress controller which is used to manage external access to the clusterkafka
contains a three-node Kafka cluster that can be used by your applicationobaas-admin
contains the Oracle Backend for Spring Boot and Microservices administration server that manages deployment of your servicesobservability
contains Jaeger tracing which is used for viewing distributed tracesopen-telemetry
contains the Open Telemetry Collector which is used to collect distributed tracing information for your servicesoracle-database-operator-system
contains the Oracle Database Operator for Kubernetes which can be used to manage Oracle Databases in Kubernetes environmentsotmm
contains Oracle Transaction Manager for Microservices which is used to manage transactions across servicesprometheus
contains Prometheus which collects metrics about your services and makes the available to Grafana for alerting and dashboardsvault
contains HashiCorp Vault which can be used to store secret or sensitive information for services, like credentials for exampleKubernetes namespaces contain other resources like pods, services, secrets and config maps. You will explore some of these now.
+Explore pods
+Kubernetes runs workloads in “pods.” Each pod can container one or more containers. There are different kinds of groupings of pods that handle scaling in different ways. Use this command to review the pods in the apisix
namespace:
$ kubectl -n apisix get pods
+ NAME READY STATUS RESTARTS AGE
+ apisix-558f6f64c6-ff6xf 1/1 Running 0 4h57m
+ apisix-dashboard-6f865fcb7b-n76c7 1/1 Running 4 (4h56m ago) 4h57m
+ apisix-etcd-0 1/1 Running 0 4h57m
+ apisix-etcd-1 1/1 Running 0 4h57m
+ apisix-etcd-2 1/1 Running 0 4h57m
The first pod listed is the APISIX API Gateway itself. It is part of a Kubernetes “deployment”. The next pod is running the APISIX Dashboard user interface - there is only one instance of that pod running. And the last three pods are running the etcd cluster that APISIX is using to store its state. These three pods are part of a “stateful set”.
+To see details of the deployments and stateful set in this namespace use this command:
+$ kubectl -n apisix get deploy,statefulset
+NAME READY UP-TO-DATE AVAILABLE AGE
+deployment.apps/apisix 3/3 3 3 6d18h
+deployment.apps/apisix-dashboard 1/1 1 1 6d18h
+
+NAME READY AGE
+statefulset.apps/apisix-etcd 3/3 6d18h
If you want to view extended information about any object you can specify its name and the output format, as in this example:
+$ kubectl -n apisix get pod apisix-etcd-0 -o yaml
Explore services
+Kubernetes services are essentially small load balancers that sit in front of groups of pods and provide a stable network address as well as load balancing. To see the services in the apisix
namespace use this command:
$ kubectl -n apisix get svc
+ NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ apisix-admin ClusterIP 10.96.26.213 <none> 9180/TCP 4h59m
+ apisix-dashboard ClusterIP 10.96.123.62 <none> 80/TCP 4h59m
+ apisix-etcd ClusterIP 10.96.54.248 <none> 2379/TCP,2380/TCP 4h59m
+ apisix-etcd-headless ClusterIP None <none> 2379/TCP,2380/TCP 4h59m
+ apisix-gateway NodePort 10.96.134.86 <none> 80:32130/TCP 4h59m
+ apisix-prometheus-metrics ClusterIP 10.96.31.169 <none> 9091/TCP 4h59m
Notice that the services give information about the ports. You can get detailed information about a service by specifying its name and output format as you did earlier for a pod.
+Explore secrets
+Sensitive information in Kubernetes is often kept in secrets that are mounted into the pods at runtime. This means that the container images do not need to have the sensitive information stored in them. It also helps with deploying to different environments where sensitive information like URLs and credentials for databases changes based on the environment.
+Oracle Backend for Spring Boot and Microservices creates a number of secrets for you so that your applications can securely access the Oracle Autonomous Database instance. Review the secrets in the pre-created application
namespace using this command. Note, the name of the secrets will be different in your environment depending on the application name you gave when deploying the application.
$ kubectl -n application get secret
+ NAME TYPE DATA AGE
+ account-db-secrets Opaque 4 57m
+ admin-liquibasedb-secrets Opaque 5 56m
+ checks-db-secrets Opaque 4 57m
+ customer-db-secrets Opaque 4 56m
+ encryption-secret-key Opaque 1 5h1m
+ public-key Opaque 1 5h1m
+ registry-auth kubernetes.io/dockerconfigjson 1 5h
+ registry-login Opaque 5 5h
+ registry-pull-auth kubernetes.io/dockerconfigjson 1 5h
+ registry-push-auth kubernetes.io/dockerconfigjson 1 5h
+ testrunner-db-secrets Opaque 4 56m
+ tls-certificate kubernetes.io/tls 5 5h
+ zimbadb-db-secrets Opaque 5 5h
+ zimbadb-tns-admin Opaque 9 5h
Whenever you create a new application namespace with the CLI and bind it to the database, these secrets will be automatically created for you in that namespace. There will two secrets created for the database, one contains the credentials to access the Oracle Autonomous Database. The other one contains the database client configuration files (tnsadmin.ora
, sqlnet.ora
, the keystores, and so on). The name of the secret depends on the application name you gave (or got autogenerated) during install, in the example above the application name is zimba
.
You can view detailed information about a secret with a command like this, you will need to provide the name of your secret which will be based on the name you chose during installation (your output will be different). Note that the values are uuencoded in this output:
+$ kubectl -n application get secret zimbadb-db-secrets -o yaml
+ apiVersion: v1
+ data:
+ db.name: xxxxxxxxxx
+ db.password: xxxxxxxxxx
+ db.service: xxxxxxxxxx
+ db.username: xxxxxxxxxx
+ secret: xxxxxxxxxx
+ kind: Secret
+ metadata:
+ creationTimestamp: "2024-05-08T16:38:06Z"
+ moduleels:
+ app.kubernetes.io/version: 1.2.0
+ name: zimbadb-db-secrets
+ namespace: application
+ resourceVersion: "3486"
+ uid: 66855e8d-22a5-4e24-b3df-379dd033ed1f
+ type: Opaque
When you deploy a Spring Boot microservice application into Oracle Backend for Spring Boot and Microservices, the pods that are created will have the values from this secret injected as environment variables that are referenced from the application.yaml
to connect to the database. The xxxxxx-tns-admin
secret will be mounted in the pod to provide access to the configuration and keystores to allow your application to authenticate to the database.
Next, you will create the “Check Processing” microservice which you will receive messages from the ATM and Back Office and process them by calling the appropriate endpoints on the Account service. This service will also introduce the use of service discovery using OpenFeign clients.
+checks
service.In the Explorer of VS Code open Java Project
and click the plus sign to add a Java Project to your workspace.
Select Spring Boot Project.
+ +Select Maven Project.
+ +Specify 3.3.1
as the Spring Boot version.
Use com.example
as the Group Id.
Enter checks
as the Artifact Id.
Use JAR
as the Packaging Type.
Select Java version 21
.
Search for Spring Web
, Lombok
, Feign
and Eureka Client
. When all are selected press Enter.
Press Enter to continue and create the Java Project
+ +Select the root
location for your project e.g. side by side with the checks
, testrunner
and account
projects.
When the project opens click Add to Workspace
+ +pom.xml
file for Oracle Spring Boot StartersIt is very similar to the POM for the account and test runner services, however the dependencies are slightly different. This service will use the “Web” Spring Boot Starter which will allow it to expose REST endpoints and make REST calls to other services. It also uses the two Oracle Spring Boot Starters for UCP and Wallet to access the database. You will also add the Eureka client and OpenFeign dependencies to allow service discovery and client side load balancing. Open the pom.xml
and add the following to the pom.xml
:
```xml
+
+<dependency>
+ <groupId>com.oracle.database.spring</groupId>
+ <artifactId>oracle-spring-boot-starter-aqjms</artifactId>
+ <version>23.4.0</version>
+</dependency>
+<dependency>
+ <groupId>com.oracle.database.spring</groupId>
+ <artifactId>oracle-spring-boot-starter-wallet</artifactId>
+ <type>pom</type>
+ <version>23.4.0</version>
+</dependency>
+```
+
+In the checks
project, rename the file called application.properties
to application.yaml
located in the src/main/resources
. This will be the Spring Boot application configuration file. Add the following content:
```yaml
+spring:
+ application:
+ name: checks
+
+ datasource:
+ url: ${spring.datasource.url}
+ username: ${spring.datasource.username}
+ password: ${spring.datasource.password}
+
+eureka:
+ instance:
+ hostname: ${spring.application.name}
+ preferIpAddress: true
+ client:
+ service-url:
+ defaultZone: ${eureka.service-url}
+ fetch-registry: true
+ register-with-eureka: true
+ enabled: true
+```
+
+This is the Spring Boot application YAML file, which contains the configuration information for this service. In this case, you need to provide the application name and the connection details for the database hosting the queues and the information for the Eureka server as the checks application will use a Feign client.
+In the checks
directory, create a new directory called src/main/java/com/example/checks
and in that directory, create a new Java file called ChecksApplication.java
with this content. This is a standard Spring Boot main class, notice the SpringBootApplication
annotation on the class. It also has the EnableJms
annotation which tells Spring Boot to enable JMS functionality in this application. The main
method is a normal Spring Boot main method:
```java
+package com.example.checks;
+
+import jakarta.jms.ConnectionFactory;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.jms.annotation.EnableJms;
+import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
+import org.springframework.jms.config.JmsListenerContainerFactory;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
+import org.springframework.jms.support.converter.MessageConverter;
+import org.springframework.jms.support.converter.MessageType;
+
+@SpringBootApplication
+@EnableFeignClients
+@EnableJms
+public class ChecksApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ChecksApplication.class, args);
+ }
+
+ @Bean // Serialize message content to json using TextMessage
+ public MessageConverter jacksonJmsMessageConverter() {
+ MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
+ converter.setTargetType(MessageType.TEXT);
+ converter.setTypeIdPropertyName("_type");
+ return converter;
+ }
+
+ @Bean
+ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
+ JmsTemplate jmsTemplate = new JmsTemplate();
+ jmsTemplate.setConnectionFactory(connectionFactory);
+ jmsTemplate.setMessageConverter(jacksonJmsMessageConverter());
+ return jmsTemplate;
+ }
+
+ @Bean
+ public JmsListenerContainerFactory<?> factory(ConnectionFactory connectionFactory,
+ DefaultJmsListenerContainerFactoryConfigurer configurer) {
+ DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
+ // This provides all boot's default to this factory, including the message converter
+ configurer.configure(factory, connectionFactory);
+ // You could still override some of Boot's default if necessary.
+ return factory;
+ }
+
+}
+```
+
+As in the Test Runner service, you will also need the MessageConverter
and JmsTemplate
beans. You will also need an additional bean in this service, the JmsListenerConnectionFactory
. This bean will be used to create listeners that receive messages from JMS queues. Note that the JMS ConnectionFactory
is injected as in the Test Runner service.
Create a directory called src/main/java/com/example/testrunner/model
and in that directory create the two model classes.
Note: These are in the testrunner
package, not the checks
package! The classes used for serialization and deserialization of the messages need to be the same so that the MessageConverter
knows what to do.
First, CheckDeposit.java
with this content:
```java
+package com.example.testrunner.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class CheckDeposit {
+ private long accountId;
+ private long amount;
+}
+```
+
+And then, Clearance.java
with this content:
```java
+package com.example.testrunner.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class Clearance {
+ private long journalId;
+}
+```
+
+++OpenFeign +In this step you will use OpenFeign to create a client. OpenFeign allows you to look up an instance of a service from the Spring Eureka Service Registry using its key/identifier, and will create a client for you to call endpoints on that service. It also provides client-side load balancing. This allows you to easily create REST clients without needing to know the address of the service or how many instances are running.
+
Create a directory called src/main/java/com/example/checks/clients
and in this directory create a new Java interface called AccountClient.java
to define the OpenFeign client for the account service. Here is the content:
```java
+package com.example.checks.clients;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient("account")
+public interface AccountClient {
+
+ @PostMapping("/api/v1/account/journal")
+ void journal(@RequestBody Journal journal);
+
+ @PostMapping("/api/v1/account/journal/{journalId}/clear")
+ void clear(@PathVariable long journalId);
+
+}
+```
+
+In the interface, you define methods for each of the endpoints you want to be able to call. As you see, you specify the request type with an annotation, the endpoint path, and you can specify path variables and the body type. You will need to define the Journal
class.
In the same directory, create a Java class called Journal.java
with the following content:
```java
+package com.example.checks.clients;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Journal {
+ private long journalId;
+ private String journalType;
+ private long accountId;
+ private String lraId;
+ private String lraState;
+ private long journalAmount;
+
+ public Journal(String journalType, long accountId, long journalAmount) {
+ this.journalType = journalType;
+ this.accountId = accountId;
+ this.journalAmount = journalAmount;
+ this.lraId = "0";
+ this.lraState = "";
+ }
+}
+```
+
+Note: The lraId
and lraState
field are set to reasonable default values, since we are not going to be using those fields in this lab.
Next, you will create a service to implement the methods defined in the OpenFeign client interface. Create a directory called src/main/java/com/example/checks/service
and in that directory create a Java class called AccountService.java
with this content. The services are very simple, you just need to use the accountClient
to call the appropriate endpoint on the Account service and pass through the data. Note the AccountClient
will be injected by Spring Boot because of the RequiredArgsConstructor
annotation, which saves some boilerplate constructor code:
```java
+package com.example.checks.service;
+
+import org.springframework.stereotype.Service;
+
+import com.example.checks.clients.AccountClient;
+import com.example.checks.clients.Journal;
+import com.example.testrunner.model.Clearance;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class AccountService {
+
+ private final AccountClient accountClient;
+
+ public void journal(Journal journal) {
+ accountClient.journal(journal);
+ }
+
+ public void clear(Clearance clearance) {
+ accountClient.clear(clearance.getJournalId());
+ }
+
+}
+```
+
+This controller will receive messages on the deposits
JMS queue and process them by calling the journal
method in the AccountService
that you just created, which will make a REST POST to the Account service, which in turn will write the journal entry into the accounts’ database.
Create a directory called src/main/java/com/example/checks/controller
and in that directory, create a new Java class called CheckReceiver.java
with the following content. You will need to inject an instance of the AccountService
(in this example the constructor is provided, so you can compare to the annotation used previously). Implement a method to receive and process the messages. To receive messages from the queues, use the JmsListener
annotation and provide the queue and factory names. This method should call the journal
method on the AccountService
and pass through the necessary data. Also, notice that you need to add the Component
annotation to the class so that Spring Boot will load an instance of it into the application:
```java
+package com.example.checks.controller;
+
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.stereotype.Component;
+
+import com.example.checks.clients.Journal;
+import com.example.checks.service.AccountService;
+import com.example.testrunner.model.CheckDeposit;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+public class CheckReceiver {
+
+ private AccountService accountService;
+
+ public CheckReceiver(AccountService accountService) {
+ this.accountService = accountService;
+ }
+
+ @JmsListener(destination = "deposits", containerFactory = "factory")
+ public void receiveMessage(CheckDeposit deposit) {
+ log.info("Received deposit <" + deposit + ">");
+ accountService.journal(new Journal("PENDING", deposit.getAccountId(), deposit.getAmount()));
+ }
+
+}
+```
+
+In the same directory, create another Java class called ClearanceReceiver.java
with the following content. This is very similar to the previous controller, but listens to the clearances
queue instead, and calls the clear
method on the AccountService
:
```java
+package com.example.checks.controller;
+
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.stereotype.Component;
+
+import com.example.checks.service.AccountService;
+import com.example.testrunner.model.Clearance;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+public class ClearanceReceiver {
+
+ private AccountService accountService;
+
+ public ClearanceReceiver(AccountService accountService) {
+ this.accountService = accountService;
+ }
+
+ @JmsListener(destination = "clearances", containerFactory = "factory")
+ public void receiveMessage(Clearance clearance) {
+ log.info("Received clearance <" + clearance + ">");
+ accountService.clear(clearance);
+ }
+
+}
+```
+
+That completes the Check Processing service. Now you can deploy and test it.
+Run the following command to build the JAR file.
+```shell
+$ mvn clean package -DskipTests
+```
+
+The service is now ready to deploy to the backend.
+The Oracle Backend for Spring Boot and Microservices admin service is not exposed outside the Kubernetes cluster by default. Oracle recommends using a kubectl port forwarding tunnel to establish a secure connection to the admin service.
+Start a tunnel using this command in a new terminal window:
+```shell
+$ kubectl -n obaas-admin port-forward svc/obaas-admin 8080
+```
+
+Get the password for the obaas-admin
user. The obaas-admin
user is the equivalent of the admin or root user in the Oracle Backend for Spring Boot and Microservices backend.
```shell
+$ kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d
+```
+
+Start the Oracle Backend for Spring Boot and Microservices CLI (oractl) in a new terminal window using this command:
+```shell
+$ oractl
+ _ _ __ _ ___
+/ \ |_) _. _. (_ / | |
+\_/ |_) (_| (_| __) \_ |_ _|_
+========================================================================================
+ Application Name: Oracle Backend Platform :: Command Line Interface
+ Application Version: (1.2.0)
+ :: Spring Boot (v3.3.0) ::
+
+ Ask for help:
+ - Slack: https://oracledevs.slack.com/archives/C03ALDSV272
+ - email: obaas_ww@oracle.com
+
+oractl:>
+```
+
+Connect to the Oracle Backend for Spring Boot and Microservices admin service using the connect
command. Enter obaas-admin
and the username and use the password you collected earlier.
```shell
+oractl> connect
+username: obaas-admin
+password: **************
+Credentials successfully authenticated! obaas-admin -> welcome to OBaaS CLI.
+oractl:>
+```
+
+Create a binding so the Check service can access the Oracle Autonomous Database as the account
user. Run this command to create the binding, and type in the password for the account
user when prompted. The password is Welcome1234##
:
```shell
+oractl:> bind --app-name application --service-name checks --username account
+```
+
+You will now deploy your Check service to the Oracle Backend for Spring Boot and Microservices using the CLI. Run this command to deploy your service, make sure you provide the correct path to your JAR file. Note that this command may take 1-3 minutes to complete:
+```shell
+oractl:> deploy --app-name application --service-name checks --artifact-path /path/to/checks-0.0.1-SNAPSHOT.jar --image-version 0.0.1
+uploading: testrunner/target/testrunner-0.0.1-SNAPSHOT.jarbuilding and pushing image...
+creating deployment and service... successfully deployed
+oractl:>
+```
+
+You can close the port forwarding session for the CLI now (just type a Ctrl+C in its console window).
+Since you had messages already sitting on the queues, the service should process those as soon as it starts. You can check the service logs to see the log messages indicating this happened using this command:
+```shell
+$ kubectl -n application logs svc/checks
+( ... lines omitted ...)
+Received deposit <CheckDeposit(accountId=2, amount=200)>
+Received clearance <Clearance(journalId=4)>
+( ... lines omitted ...)
+```
+
+You can also look at the journal table in the database to see the results.
+ + +Connect to the database as the ADMIN
user and execute the following statements to give the account
user the necessary permissions to use queues. Note: module 2, Task 9 provided details on how to connect to the database.
```sql
+grant execute on dbms_aq to account;
+grant execute on dbms_aqadm to account;
+grant execute on dbms_aqin to account;
+commit;
+```
+
+Now connect as the account
user and create the queues by executing these statements (replace [TNS-ENTRY]
with your environment information). You can get the TNS Entries by executing SHOW TNS
in the sql shell:
```sql
+connect account/Welcome1234##@[TNS-ENTRY];
+
+begin
+ -- deposits
+ dbms_aqadm.create_queue_table(
+ queue_table => 'deposits_qt',
+ queue_payload_type => 'SYS.AQ$_JMS_TEXT_MESSAGE');
+ dbms_aqadm.create_queue(
+ queue_name => 'deposits',
+ queue_table => 'deposits_qt');
+ dbms_aqadm.start_queue(
+ queue_name => 'deposits');
+ -- clearances
+ dbms_aqadm.create_queue_table(
+ queue_table => 'clearances_qt',
+ queue_payload_type => 'SYS.AQ$_JMS_TEXT_MESSAGE');
+ dbms_aqadm.create_queue(
+ queue_name => 'clearances',
+ queue_table => 'clearances_qt');
+ dbms_aqadm.start_queue(
+ queue_name => 'clearances');
+end;
+/
+```
+
+You have created two queues named deposits
and clearances
. Both of them use the JMS TextMessage
format for the payload.
Next, you will create the “Test Runner” microservice which you will use to simulate the ATM and Back Office. This service will send messages to the queues that you just created.
+transfer
service.In the Explorer of VS Code open Java Project
and click the plus sign to add a Java Project to your workspace.
Select Spring Boot Project.
+ +Select Maven Project.
+ +Specify 3.3.1
as the Spring Boot version.
Use com.example
as the Group Id.
Enter testrunner
as the Artifact Id.
Use JAR
as the Packaging Type.
Select Java version 21
.
Search for Spring Web
and press Enter
Press Enter to continue and create the Java Project
+ +Select the root
location for your project e.g. side by side with the account
project.
When the project opens click Add to Workspace
+ +pom.xml
fileOpen the pom.xml
file in the testrunner
project. This service will use the “Web” Spring Boot Starter which will allow it to expose REST endpoints and make REST calls to other services. It also uses the two Oracle Spring Boot Starters for UCP and Wallet to access the database: Add the following to the pom.xml:
```xml
+
+ <dependency>
+ <groupId>com.oracle.database.spring</groupId>
+ <artifactId>oracle-spring-boot-starter-aqjms</artifactId>
+ <version>23.4.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.oracle.database.spring</groupId>
+ <artifactId>oracle-spring-boot-starter-wallet</artifactId>
+ <type>pom</type>
+ <version>23.4.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+```
+
+In the testrunner
project, rename the file called application.properties
to application.yaml
located in the src/main/resources
. This will be the Spring Boot application configuration file:
```yaml
+spring:
+ application:
+ name: testrunner
+
+ datasource:
+ url: ${spring.datasource.url}
+ username: ${spring.datasource.username}
+ password: ${spring.datasource.password}
+```
+
+This is the Spring Boot application YAML file, which contains the configuration information for this service. In this case, you only need to provide the application name and the connection details for the database hosting the queues.
+In the testrunner
directory, open the Java file called TestrunnerApplication.java
and add this content. This is a standard Spring Boot main class, notice the @SpringBootApplication
annotation on the class. It also has the @EnableJms
annotation which tells Spring Boot to enable JMS functionality in this application. The main
method is a normal Spring Boot main method:
```java
+
+package com.example.testrunner;
+
+import jakarta.jms.ConnectionFactory;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.jms.annotation.EnableJms;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
+import org.springframework.jms.support.converter.MessageConverter;
+import org.springframework.jms.support.converter.MessageType;
+
+@SpringBootApplication
+@EnableJms
+public class TestrunnerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestrunnerApplication.class, args);
+ }
+
+ // Serialize message content to json using TextMessage
+ @Bean
+ public MessageConverter jacksonJmsMessageConverter() {
+ MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
+ converter.setTargetType(MessageType.TEXT);
+ converter.setTypeIdPropertyName("_type");
+ return converter;
+ }
+
+ @Bean
+ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
+ JmsTemplate jmsTemplate = new JmsTemplate();
+ jmsTemplate.setConnectionFactory(connectionFactory);
+ jmsTemplate.setMessageConverter(jacksonJmsMessageConverter());
+ return jmsTemplate;
+ }
+
+}
+
+```
+
+In addition to the standard parts of a Spring Boot application class, you will add two beans that will be needed in this service. First, you need a MessageConverter
bean so that you can convert a Java object (POJO) into JSON format, and vice versa. This bean will be used to serialize and deserialize the objects you need to write onto the queues.
The second bean you need is a JmsTemplate
. This is a standard Spring JMS bean that is used to access JMS functionality. You will use this bean to enqueue messages. Notice that this bean is configured to use the MessageConverter
bean and that the JMS ConnectionFactory
is injected. The Oracle Spring Boot Starter for AQ/JMS will create the JMS ConnectionFactory
for you.
Note: The Oracle Spring Boot Starter for AQ/JMS will also inject a JDBC Connection
bean which shares the same database transaction with the JMS ConnectionFactory
. This is not needed in this lab. The shared transaction enables you to write methods which can perform both JMS and JPA operations in an atomic transaction, which can be very helpful in some use cases and can dramatically reduce the amount of code needed to handle situations like duplicate message delivery or lost messages.
Create a new directory called src/main/java/com/example/testrunner/model
and in this directory create two Java files. First, CheckDeposit.java
with this content. This class will be used to simulate the ATM sending the “deposit” notification:
```java
+package com.example.testrunner.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class CheckDeposit {
+ private long accountId;
+ private long amount;
+}
+```
+
+Next, Clearance.java
with this content. This class will be used to simulate the Back Office sending the “clearance” notification:
```java
+package com.example.testrunner.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Clearance {
+ private long journalId;
+}
+```
+
+Create a new directory called src/main/java/com/example/testrunner/controller
and in this directory create a new Java file called TestRunnerController.java
with the following content. This class will have the RestController
annotation so that it can expose REST APIs that you can call to trigger the simulation of the ATM and Back Office notifications. It will need the JmsTemplate
to access JMS functionality, this can be injected with the AutoWired
annotation. Create two methods, one to send each notification:
```java
+package com.example.testrunner.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.example.testrunner.model.CheckDeposit;
+import com.example.testrunner.model.Clearance;
+
+@RestController
+@RequestMapping("/api/v1/testrunner")
+public class TestRunnerController {
+
+ @Autowired
+ private JmsTemplate jmsTemplate;
+
+ @PostMapping("/deposit")
+ public ResponseEntity<CheckDeposit> depositCheck(@RequestBody CheckDeposit deposit) {
+ jmsTemplate.convertAndSend("deposits", deposit);
+ return new ResponseEntity<CheckDeposit>(deposit, HttpStatus.CREATED);
+ }
+
+ @PostMapping("/clear")
+ public ResponseEntity<Clearance> clearCheck(@RequestBody Clearance clearance) {
+ jmsTemplate.convertAndSend("clearances", clearance);
+ return new ResponseEntity<Clearance>(clearance, HttpStatus.CREATED);
+ }
+
+}
+```
+
+Run the following command to build the JAR file.
+```shell
+$ mvn clean package -DskipTests
+```
+
+The service is now ready to deploy to the backend.
+The Oracle Backend for Spring Boot and Microservices admin service is not exposed outside the Kubernetes cluster by default. Oracle recommends using a kubectl port forwarding tunnel to establish a secure connection to the admin service.
+Start a tunnel using this command in a new terminal window:
+
+```shell
+$ kubectl -n obaas-admin port-forward svc/obaas-admin 8080
+```
+
+Get the password for the obaas-admin
user. The obaas-admin
user is the equivalent of the admin or root user in the Oracle Backend for Spring Boot and Microservices backend.
```shell
+$ kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d
+```
+
+Start the Oracle Backend for Spring Boot and Microservices CLI (oractl) in a new terminal window using this command:
+```shell
+$ oractl
+ _ _ __ _ ___
+/ \ |_) _. _. (_ / | |
+\_/ |_) (_| (_| __) \_ |_ _|_
+========================================================================================
+ Application Name: Oracle Backend Platform :: Command Line Interface
+ Application Version: (1.2.0)
+ :: Spring Boot (v3.3.0) ::
+
+ Ask for help:
+ - Slack: https://oracledevs.slack.com/archives/C03ALDSV272
+ - email: obaas_ww@oracle.com
+
+oractl:>
+```
+
+Connect to the Oracle Backend for Spring Boot and Microservices admin service using the `connect` command. Enter `obaas-admin` and the username and use the password you collected earlier.
+
+```shell
+oractl> connect
+username: obaas-admin
+password: **************
+Credentials successfully authenticated! obaas-admin -> welcome to OBaaS CLI.
+oractl:>
+```
+
+Create a binding so the Test Runner service can access the Oracle Autonomous Database as the account
user. Run this command to create the binding, and type in the password for the account
user when prompted. The password is Welcome1234##
:
```shell
+oractl:> bind --app-name application --service-name testrunner --username account
+```
+
+You will now deploy your Test Runner service to the Oracle Backend for Spring Boot and Microservices using the CLI. Run this command to deploy your service, make sure you provide the correct path to your JAR file. Note that this command may take 1-3 minutes to complete:
+```shell
+oractl:> deploy --app-name application --service-name testrunner --artifact-path /path/to/testrunner-0.0.1-SNAPSHOT.jar --image-version 0.0.1
+uploading: testrunner/target/testrunner-0.0.1-SNAPSHOT.jarbuilding and pushing image...
+creating deployment and service... successfully deployed
+oractl:>
+```
+
+You can close the port forwarding session for the CLI now (just type a Ctrl+C in its console window).
+
+testrunner
service is runningVerify that the testrunner application is up and running by running this command:
+```shell
+$ kubectl logs -n application svc/testrunner
+```
+
+The output should be similar to this, look for Started TestrunnerApplication
```text
+2023-06-02 15:18:39.620 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1806 ms
+2023-06-02 15:18:40.915 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
+2023-06-02 15:18:40.938 INFO 1 --- [ main] c.e.testrunner.TestrunnerApplication : Started TestrunnerApplication in 4.174 seconds (JVM running for 5.237)
+```
+
+The Test Runner service is not exposed outside your Kubernetes cluster, so you must create a port-forwarding tunnel to access it. Create a tunnel using this command:
+```shell
+$ kubectl -n application port-forward svc/testrunner 8084:8080
+```
+
+Call the deposit endpoint to send a deposit notification using this command:
+```shell
+$ curl -i -X POST -H 'Content-Type: application/json' -d '{"accountId": 2, "amount": 200}' http://localhost:8084/api/v1/testrunner/deposit
+HTTP/1.1 201
+Date: Wed, 31 May 2023 15:11:55 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"accountId":2,"amount":200}
+```
+
+Call the clear endpoint to send a clearance notification using this command. Note that you can use any journalId
since there is nothing receiving and processing these messages yet:
```shell
+$ curl -i -X POST -H 'Content-Type: application/json' -d '{"journalId": 4}' http://localhost:8084/api/v1/testrunner/clear
+HTTP/1.1 201
+Date: Wed, 31 May 2023 15:12:54 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"journalId":4}
+```
+
+Connect to the database as the account
(password Welcome12343##
) and issue this SQL statement to check the payloads of the messages on the deposits queue:
```sql
+SQL> select qt.user_data.text_vc from deposits_qt qt;
+
+USER_DATA.TEXT_VC
+_______________________________
+{"accountId":2,"amount":200}
+```
+
+Issue this SQL statement to check the payloads of the messages on the clearances queue:
+```sql
+SQL> select qt.user_data.text_vc from clearances_qt qt;
+
+USER_DATA.TEXT_VC
+____________________
+{"journalId":4}
+```
+
+hat completes the Test Runner service. Next, you will build the Check Processing service which will receive these messages and process them.
+ + +This is a new chapter.
+ + +This module walks you through the steps to build Spring Boot microservices that use Java Message Service (JMS) to send and receive asynchronous messages using Transactional Event Queues in the Oracle Database. This service will also use service discovery (OpenFeign) to look up and use the previously built Account service. In this lab, we will extend the Account microservice built in the previous lab, build a new “Check Processing” microservice and another “Test Runner” microservice to help with testing.
+Estimated Time: 20 minutes
+In this lab, you will:
+This module assumes you have:
+In the previous lab, you created an Account service that includes endpoints to create and query accounts, lookup accounts for a given customer, and so on. In this module you will extend that service to add some new endpoints to allow recording bank transactions, in this case check deposits, in the account journal.
+In this lab, we will assume that customers can deposit a check at an Automated Teller Machine (ATM) by typing in the check amount, placing the check into a deposit envelope and then inserting that envelope into the ATM. When this occurs, the ATM will send a “deposit” message with details of the check deposit. You will record this as a “pending” deposit in the account journal.
+ +Later, imagine that the deposit envelop arrives at a back office check processing facility where a person checks the details are correct, and then “clears” the check. When this occurs, a “clearance” message will be sent. Upon receiving this message, you will change the “pending” transaction to a finalized “deposit” in the account journal.
+ +You will implement this using three microservices:
+Now you can test the full end-to-end flow for the Check Processing scenario.
+The Test Runner service is not exposed outside your Kubernetes cluster, so you must create a port-forwarding tunnel to access it. Create a tunnel using this command:
+```shell
+$ kubectl -n application port-forward svc/testrunner 8084:8080
+```
+
+Simulate a check being deposited at the ATM using the Test Runner service:
+```shell
+$ curl -i -X POST -H 'Content-Type: application/json' -d '{"accountId": 2, "amount": 256}' http://localhost:8084/api/v1/testrunner/deposit
+HTTP/1.1 201
+Date: Wed, 31 May 2023 15:11:55 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"accountId":2,"amount":256}
+```
+
+Check the logs for the Check Processing service using this command. You should see a log message indicating that the message was received and processed:
+```shell
+$ kubectl -n application logs svc/checks
+( ... lines omitted ...)
+Received deposit <CheckDeposit(accountId=2, amount=256)>
+( ... lines omitted ...)
+```
+
+In the next commands, you need to provide the correct IP address for the API Gateway in your backend environment. You can find the IP address using this command, you need the one listed in the EXTERNAL-IP
column:
```shell
+$ kubectl -n ingress-nginx get service ingress-nginx-controller
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ingress-nginx-controller LoadBalancer 10.123.10.127 100.20.30.40 80:30389/TCP,443:30458/TCP 13d
+```
+
+Use this command to retrieve the journal entries for this account. Your output may contain more entries. Find the entry corresponding to the deposit you just simulated (it was for $256) and note the journalId
- you will need it in the next step:
```shell
+$ curl -i http://[EXTERNAL-IP]/api/v1/account/2/journal
+HTTP/1.1 200
+Date: Wed, 31 May 2023 13:03:22 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+[{"journalId":6,"journalType":"PENDING","accountId":2,"lraId":"0","lraState":null,"journalAmount":256}]
+```
+
+Using the journalId
you received in the output of the previous command (in this example it is 6
), update and then run this command to simulate the Back Office clearing that check:
```shell
+$ curl -i -X POST -H 'Content-Type: application/json' -d '{"journalId": 6}' http://localhost:8084/api/v1/testrunner/clear
+HTTP/1.1 201
+Date: Wed, 31 May 2023 15:12:54 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"journalId":6}
+```
+
+Check the logs for the Check Processing service using this command. You should see a log message indicating that the message was received and processed:
+```shell
+$ kubectl -n application logs svc/checks
+( ... lines omitted ...)
+Received clearance <Clearance(journalId=6)>
+( ... lines omitted ...)
+```
+
+Retrieve the journal entries again to confirm the PENDING
entry was updated to a DEPOSIT
:
```shell
+$ curl -i http://[EXTERNAL-IP]/api/v1/account/2/journal
+HTTP/1.1 200
+Date: Wed, 31 May 2023 13:03:22 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+[{"journalId":6,"journalType":"DEPOSIT","accountId":2,"lraId":"0","lraState":null,"journalAmount":256}]
+```
+
+That completes this lab, congratulations, you learned how to use JMS to create loosely coupled services that process asynchronous messages, and also how to use service discovery with OpenFeign.
+ + +Starting with the account service that you built in the previous lab, you will the JPA model and repository for the journal and some new endpoints.
+Create a new Java file in src/main/java/com/example/accounts/model
called Journal.java
. In this class you can define the fields that make up the journal. Note that you created the Journal table in the previous lab. You will not use the lraId
and lraState
fields until a later lab. To simplify this lab, create an additional constructor that defaults those fields to suitable values. Your new class should look like this:
```java
+package com.example.account.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "JOURNAL")
+@Data
+@NoArgsConstructor
+public class Journal {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "JOURNAL_ID")
+ private long journalId;
+
+ // type is withdraw or deposit
+ @Column(name = "JOURNAL_TYPE")
+ private String journalType;
+
+ @Column(name = "ACCOUNT_ID")
+ private long accountId;
+
+ @Column(name = "LRA_ID")
+ private String lraId;
+
+ @Column(name = "LRA_STATE")
+ private String lraState;
+
+ @Column(name = "JOURNAL_AMOUNT")
+ private long journalAmount;
+
+ public Journal(String journalType, long accountId, long journalAmount) {
+ this.journalType = journalType;
+ this.accountId = accountId;
+ this.journalAmount = journalAmount;
+ }
+
+ public Journal(String journalType, long accountId, long journalAmount, String lraId, String lraState) {
+ this.journalType = journalType;
+ this.accountId = accountId;
+ this.lraId = lraId;
+ this.lraState = lraState;
+ this.journalAmount = journalAmount;
+ }
+}
+```
+
+Create a new Java file in src/main/java/com/example/account/repository
called JournalRepository.java
. This should be an interface that extends JpaRepository
and you will need to define a method to find journal entries by accountId
. Your interface should look like this:
```java
+package com.example.account.repository;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.example.account.model.Journal;
+
+public interface JournalRepository extends JpaRepository<Journal, Long> {
+ List<Journal> findJournalByAccountId(long accountId);
+}
+```
+
+AccountController
constructorUpdate the constructor for AccountController
so that both the repositories are injected. You will need to create a variable to hold each. Your updated constructor should look like this:
```java
+import com.example.repository.JournalRepository;
+
+// ...
+
+final AccountRepository accountRepository;
+final JournalRepository journalRepository;
+
+public AccountController(AccountRepository accountRepository, JournalRepository journalRepository) {
+ this.accountRepository = accountRepository;
+ this.journalRepository = journalRepository;
+}
+```
+
+Add a new HTTP POST endpoint in the AccountController.java
class. The method accepts a journal entry in the request body and saves it into the database. Your new method should look like this:
```java
+import com.example.model.Journal;
+
+// ...
+
+@PostMapping("/account/journal")
+public ResponseEntity<Journal> postSimpleJournalEntry(@RequestBody Journal journalEntry) {
+ try {
+ Journal _journalEntry = journalRepository.saveAndFlush(journalEntry);
+ return new ResponseEntity<>(_journalEntry, HttpStatus.CREATED);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
+```
+
+Add a new HTTP GET endpoint in the AccountController.java
class to get a list of journal entries for a given accountId
. Your new method should look like this:
```java
+import com.example.account.repository.JournalRepository;
+
+@GetMapping("/account/{accountId}/journal")
+public List<Journal> getJournalEntriesForAccount(@PathVariable("accountId") long accountId) {
+ return journalRepository.findJournalByAccountId(accountId);
+}
+```
+
+Add a new HTTP POST endpoint to update and existing journal entry to a cleared deposit. To do this, you set the journalType
field to DEPOSIT
. Your method should accept the journalId
as a path variable. If the specified journal entry does not exist, return a 202 (Accepted) to indicate the message was received but there was nothing to do. Returning a 404 (Not found) would cause an error and the message would get requeued and reprocessed, which we don’t want. Your new method should look like this:
```java
+@PostMapping("/account/journal/{journalId}/clear")
+public ResponseEntity<Journal> clearJournalEntry(@PathVariable long journalId) {
+ try {
+ Optional<Journal> data = journalRepository.findById(journalId);
+ if (data.isPresent()) {
+ Journal _journalEntry = data.get();
+ _journalEntry.setJournalType("DEPOSIT");
+ journalRepository.saveAndFlush(_journalEntry);
+ return new ResponseEntity<Journal>(_journalEntry, HttpStatus.OK);
+ } else {
+ return new ResponseEntity<Journal>(new Journal(), HttpStatus.ACCEPTED);
+ }
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
+```
+
+Run the following command to build the JAR file. Note that you will need to skip tests now, since you updated the application.yaml
and it no longer points to your local test database instance.
```shell
+$ mvn clean package -DskipTests
+```
+
+The service is now ready to deploy to the backend.
+obaas-admin
user. The obaas-admin
user is the equivalent of the admin or root user in the Oracle Backend for Spring Boot and Microservices backend.Execute the following command to get the password:
+```shell
+$ kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d
+```
+
+The Oracle Backend for Spring Boot and Microservices admin service is not exposed outside the Kubernetes cluster by default. Oracle recommends using a kubectl port forwarding tunnel to establish a secure connection to the admin service.
+Start a tunnel (unless you already have the tunnel running from previous labs) using this command:
+```shell
+$ kubectl -n obaas-admin port-forward svc/obaas-admin 8080
+```
+
+Start the Oracle Backend for Spring Boot and Microservices CLI (oractl) using this command:
+```shell
+$ oractl
+ _ _ __ _ ___
+/ \ |_) _. _. (_ / | |
+\_/ |_) (_| (_| __) \_ |_ _|_
+========================================================================================
+ Application Name: Oracle Backend Platform :: Command Line Interface
+ Application Version: (1.2.0)
+ :: Spring Boot (v3.3.0) ::
+
+ Ask for help:
+ - Slack: https://oracledevs.slack.com/archives/C03ALDSV272
+ - email: obaas_ww@oracle.com
+
+ oractl:>
+```
+
+Connect to the Oracle Backend for Spring Boot and Microservices admin service using the connect
command. Enter obaas-admin
and the username and use the password you collected earlier.
```shell
+oractl:>connect
+? username obaas-admin
+? password *************
+Credentials successfully authenticated! obaas-admin -> welcome to OBaaS CLI.
+oractl:>
+```
+
+You will now deploy your account service to the Oracle Backend for Spring Boot and Microservices using the CLI. Run this command to redeploy your service, make sure you provide the correct path to your JAR file. Note that this command may take 1-3 minutes to complete:
+```shell
+oractl:> deploy --app-name application --service-name account --artifact-path /path/to/account-0.0.1-SNAPSHOT.jar --image-version 0.0.1
+uploading: account/target/account-0.0.1-SNAPSHOT.jarbuilding and pushing image...
+creating deployment and service... successfully deployed
+oractl:>
+```
+
+In the next three commands, you need to provide the correct IP address for the API Gateway in your backend environment. You can find the IP address using this command, you need the one listed in the EXTERNAL-IP
column:
```shell
+$ kubectl -n ingress-nginx get service ingress-nginx-controller
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ingress-nginx-controller LoadBalancer 10.123.10.127 100.20.30.40 80:30389/TCP,443:30458/TCP 13d
+```
+
+Test the create journal entry endpoint (make sure you use an accountId
that exits in your database) with this command, use the IP address for your API Gateway.
```shell
+$ curl -i -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"journalType": "PENDING", "accountId": 2, "journalAmount": 100.00, "lraId": "0", "lraState": ""}' \
+ http://[EXTERNAL-IP]/api/v1/account/journal
+HTTP/1.1 201
+Date: Wed, 31 May 2023 13:02:10 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"journalId":1,"journalType":"PENDING","accountId":2,"lraId":"0","lraState":"","journalAmount":100}
+```
+
+Notice that the response contains a journalId
which you will need in a later command, and that the journalType
is PENDING
.
Test the get journal entries endpoint with this command, use the IP address for your API Gateway and the same accountId
as in the previous step. Your output may be different:
```shell
+$ curl -i http://[EXTERNAL-IP]/api/v1/account/[accountId]/journal
+HTTP/1.1 200
+Date: Wed, 31 May 2023 13:03:22 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+[{"journalId":1,"journalType":"PENDING","accountId":2,"lraId":"0","lraState":null,"journalAmount":100}]
+```
+
+Test the update/clear journal entry endpoint with this command, use the IP address for your API Gateway and the journalId
from the first command’s response:
```shell
+$ curl -i -X POST http://[EXTERNAL-IP]/api/v1/account/journal/[journalId]/clear
+HTTP/1.1 200
+Date: Wed, 31 May 2023 13:04:36 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"journalId":1,"journalType":"DEPOSIT","accountId":2,"lraId":"0","lraState":null,"journalAmount":100}
+```
+
+That completes the updates for the Account service.
+ + +This is a new chapter.
+ + +When you have finished this Live module you may wish to clean up the resources you created. If so, you may complete the steps in this optional module.
+Estimated module Time: 20 minutes
+Quick walk through on how to clean up the module environment.
+ +In this workshop, you will learn how to:
+This module assumes you have:
+The Oracle Backend for Spring Boot and Microservices environment was deployed using ORM and Terraform. The uninstall will use OCI Resource Manager (ORM) to Destroy the stack.
+Navigate to OCI Resource Manager Stacks
+ +Make sure you choose the Compartment where you installed Oracle Backend for Spring Boot and Microservices. Click on the Stack Name (which will be different from the screenshot)
+ +After picking the stack. Click destroy. NOTE This will stop all resources and remove the Oracle Backend for Spring Boot and Microservices environment. The only way to get it back is to re-deploy the stack
+ +Confirm that you want to shut down and destroy the resources
+ +If the Terraform Destroy job fails, re-run the Destroy job again after a few minutes.
+Even after the Destroy job has finished there will be one resource left in the tenancy/compartment and that is an OCI Vault. The Vault is on PENDING DELETION
mode.
s that we want to ignore */ + display: none; +} + +/* in case of image render hook, Hugo may generate empty
s that we want to ignore as well, so a simple :first-child or :last-child is not enough */ +#R-body table th > :nth-child(1 of :not(:empty)), +#R-body table th > :nth-child(1 of :not(:empty)) :nth-child(1 of :not(:empty)), +#R-body table td > :nth-child(1 of :not(:empty)), +#R-body table td > :nth-child(1 of :not(:empty)) :nth-child(1 of :not(:empty)), +#R-body div.box > .box-content > :nth-child(1 of :not(:empty)), +#R-body div.box > .box-content > :nth-child(1 of :not(:empty)) :nth-child(1 of :not(:empty)), +#R-body div.expand > .expand-content-text > :nth-child(1 of :not(:empty)), +#R-body div.expand > .expand-content-text > :nth-child(1 of :not(:empty)) :nth-child(1 of :not(:empty)), +#R-body div.tab-content > .tab-content-text > :nth-child(1 of :not(:empty)), +#R-body div.tab-content > .tab-content-text > :nth-child(1 of :not(:empty)) :nth-child(1 of :not(:empty)) { + margin-top: 0; +} + +#R-body table th > :nth-last-child(1 of :not(:empty)), +#R-body table th > :nth-last-child(1 of :not(:empty)) :nth-last-child(1 of :not(:empty)), +#R-body table th > div.highlight:last-child pre:not(.mermaid), +#R-body table td > :nth-last-child(1 of :not(:empty)), +#R-body table td > :nth-last-child(1 of :not(:empty)) :nth-last-child(1 of :not(:empty)), +#R-body table td > div:last-child pre:not(.mermaid), +#R-body div.box > .box-content > :nth-last-child(1 of :not(:empty)), +#R-body div.box > .box-content > :nth-last-child(1 of :not(:empty)) :nth-last-child(1 of :not(:empty)), +#R-body div.box > .box-content > div:last-child pre:not(.mermaid), +#R-body div.expand > .expand-content-text > :nth-last-child(1 of :not(:empty)), +#R-body div.expand > .expand-content-text > :nth-last-child(1 of :not(:empty)) :nth-last-child(1 of :not(:empty)), +#R-body div.expand > .expand-content-text > div:last-child pre:not(.mermaid), +#R-body div.tab-content > .tab-content-text > :nth-last-child(1 of :not(:empty)), +#R-body div.tab-content > .tab-content-text > :nth-last-child(1 of :not(:empty)) :nth-last-child(1 of :not(:empty)), +#R-body div.tab-content > .tab-content-text > div:last-child pre:not(.mermaid) { + margin-bottom: 0; +} + +/* resources shortcode */ + +div.attachments .box-content { + display: block; + margin: 0; + padding-inline-start: 1.75rem; +} + +/* Children shortcode */ + +.children p { + font-size: .8125rem; + margin-bottom: 0; + margin-top: 0; + padding-bottom: 0; + padding-top: 0; +} + +.children-li p { + font-size: .8125rem; + font-style: italic; +} + +.children-h2 p, +.children-h3 p { + font-size: .8125rem; + margin-bottom: 0; + margin-top: 0; + padding-bottom: 0; + padding-top: 0; +} + +#R-body-inner .children h2, +#R-body-inner .children h3, +#R-body-inner .children h4, +#R-body-inner .children h5, +#R-body-inner .children h6 { + margin-bottom: 0; + margin-top: 1rem; +} +#R-body-inner ul.children-h2, +#R-body-inner ul.children-h3, +#R-body-inner ul.children-h4, +#R-body-inner ul.children-h5, +#R-body-inner ul.children-h6 { + /* if we display children with style=h2 but without a containerstyle + a ul will be used for structuring; we remove default indention for uls + in this case */ + padding-inline-start: 0; +} + +code, +kbd, +pre:not(.mermaid), +samp { + font-size: .934375rem; + vertical-align: baseline; +} + +code { + border-radius: 2px; + border-style: solid; + border-width: 1px; + -webkit-print-color-adjust: economy; + color-adjust: economy; + padding-left: 2px; + padding-right: 2px; + white-space: nowrap; +} + +span.copy-to-clipboard { + display: inline-block; + white-space: nowrap; +} + +code.copy-to-clipboard-code { + border-end-end-radius: 0; + border-start-end-radius: 0; + border-inline-end-width: 0; +} + +pre:not(.mermaid) { + border-radius: 2px; + border-style: solid; + border-width: 1px; + -webkit-print-color-adjust: economy; + color-adjust: economy; + line-height: 1.15; + padding: 1rem; + position: relative; +} + +/* pre:not(.mermaid):has( code ), */ +/* the :has() operator isn't available in FF yet, so we patch this by JS */ +pre:not(.mermaid).pre-code { + direction: ltr; + text-align: left; +} + +pre:not(.mermaid) code { + background-color: inherit; + border: 0; + color: inherit; + -webkit-print-color-adjust: economy; + color-adjust: economy; + font-size: .9375rem; + margin: 0; + padding: 0; +} + +div.highlight{ + position: relative; +} +/* we may have special treatment if highlight shortcode was used in table lineno mode */ +div.highlight > div{ + border-style: solid; + border-width: 1px; +} +/* remove default style for usual markdown tables */ +div.highlight > div table{ + background-color: transparent; + border-width: 0; + margin: 0; +} +div.highlight > div td{ + border-width: 0; +} +#R-body div.highlight > div a { + line-height: inherit; +} +#R-body div.highlight > div a:after { + display: none; +} +/* disable selection for lineno cells */ +div.highlight > div td:first-child:not(:last-child){ + -webkit-user-select: none; + user-select: none; +} +/* increase code column to full width if highlight shortcode was used in table lineno mode */ +div.highlight > div td:not(:first-child):last-child{ + width: 100%; +} +/* add scrollbars if highlight shortcode was used in table lineno mode */ +div.highlight > div table{ + display: block; + overflow: auto; +} +div.highlight:not(.wrap-code) pre:not(.mermaid){ + overflow: auto; +} +div.highlight:not(.wrap-code) pre:not(.mermaid) code{ + white-space: pre; +} +div.highlight.wrap-code pre:not(.mermaid) code{ + white-space: pre-wrap; +} +/* remove border from row cells if highlight shortcode was used in table lineno mode */ +div.highlight > div td > pre:not(.mermaid) { + border-radius: 0; + border-width: 0; +} +/* in case of table lineno mode we want to move each row closer together - besides the edges +this usually applies only to wrapfix tables but it doesn't hurt for non-wrapfix tables too */ +div.highlight > div tr:not(:first-child) pre:not(.mermaid){ + padding-top: 0; +} +div.highlight > div tr:not(:last-child) pre:not(.mermaid){ + padding-bottom: 0; +} +/* in case of table lineno mode we want to move each columns closer together on the inside */ +div.highlight > div td:first-child:not(:last-child) pre:not(.mermaid){ + padding-right: 0; +} +div.highlight > div td:not(:first-child):last-child pre:not(.mermaid){ + padding-left: 0; +} + +hr { + border-bottom: 4px solid rgba( 134, 134, 134, .125 ); +} + +#R-body-inner pre:not(.mermaid) { + white-space: pre-wrap; +} + +table { + border: 1px solid rgba( 134, 134, 134, .333 ); + margin-bottom: 1rem; + margin-top: 1rem; + table-layout: auto; +} + +th, +thead td { + background-color: rgba( 134, 134, 134, .166 ); + border: 1px solid rgba( 134, 134, 134, .333 ); + -webkit-print-color-adjust: exact; + color-adjust: exact; + padding: 0.5rem; +} + +td { + border: 1px solid rgba( 134, 134, 134, .333 ); + padding: 0.5rem; +} +tbody > tr:nth-child(even) > td { + background-color: rgba( 134, 134, 134, .045 ); +} + +.tooltipped { + position: relative; +} + +.tooltipped:after { + background: rgba( 0, 0, 0, 1 ); + border: 1px solid rgba( 119, 119, 119, 1 ); + border-radius: 3px; + color: rgba( 255, 255, 255, 1 ); + content: attr(aria-label); + display: none; + font-family: "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-size: .6875rem; + font-weight: normal; + -webkit-font-smoothing: subpixel-antialiased; + letter-spacing: normal; + line-height: 1.5; + padding: 5px 8px; + pointer-events: none; + position: absolute; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: pre; + word-wrap: break-word; + z-index: 140; +} + +.tooltipped:before { + border: 5px solid transparent; + color: rgba( 0, 0, 0, 1 ); + content: ""; + display: none; + height: 0; + pointer-events: none; + position: absolute; + width: 0; + z-index: 150; +} + +.tooltipped:hover:before, +.tooltipped:hover:after, +.tooltipped:active:before, +.tooltipped:active:after, +.tooltipped:focus:before, +.tooltipped:focus:after { + display: inline-block; + text-decoration: none; +} + +.tooltipped-s:after, +.tooltipped-se:after, +.tooltipped-sw:after { + margin-top: 5px; + right: 50%; + top: 100%; +} + +.tooltipped-s:before, +.tooltipped-se:before, +.tooltipped-sw:before { + border-bottom-color: rgba( 0, 0, 0, .8 ); + bottom: -5px; + margin-right: -5px; + right: 50%; + top: auto; +} + +.tooltipped-se:after { + left: 50%; + margin-left: -15px; + right: auto; +} + +.tooltipped-sw:after { + margin-right: -15px; +} + +.tooltipped-n:after, +.tooltipped-ne:after, +.tooltipped-nw:after { + bottom: 100%; + margin-bottom: 5px; + right: 50%; +} + +.tooltipped-n:before, +.tooltipped-ne:before, +.tooltipped-nw:before { + border-top-color: rgba( 0, 0, 0, .8 ); + bottom: auto; + margin-right: -5px; + right: 50%; + top: -5px; +} + +.tooltipped-ne:after { + left: 50%; + margin-left: -15px; + right: auto; +} + +.tooltipped-nw:after { + margin-right: -15px; +} + +.tooltipped-s:after, +.tooltipped-n:after { + transform: translateX(50%); +} + +.tooltipped-w:after { + bottom: 50%; + margin-right: 5px; + right: 100%; + transform: translateY(50%); +} + +.tooltipped-w:before { + border-left-color: rgba( 0, 0, 0, .8 ); + bottom: 50%; + left: -5px; + margin-top: -5px; + top: 50%; +} + +.tooltipped-e:after { + bottom: 50%; + left: 100%; + margin-left: 5px; + transform: translateY(50%); +} + +.tooltipped-e:before { + border-right-color: rgba( 0, 0, 0, .8 ); + bottom: 50%; + margin-top: -5px; + right: -5px; + top: 50%; +} + +#R-topbar { + min-height: 3rem; + position: relative; + z-index: 170; +} + +#R-topbar > .topbar-wrapper { + align-items: center; + background-color: rgba( 134, 134, 134, .066 ); + display: flex; + flex-basis: 100%; + flex-direction: row; + height: 100%; +} + +.topbar-button { + display: inline-block; + position: relative; +} +.topbar-button:not([data-origin]) { + display: none; +} + +.topbar-button > .topbar-control { + display: inline-block; + padding-left: 1rem; + padding-right: 1rem; +} +.topbar-wrapper > .topbar-area-start > .topbar-button > .topbar-control { + border-inline-end: 1px solid rgba( 134, 134, 134, .333 ); +} +.topbar-wrapper > .topbar-area-end > .topbar-button > .topbar-control { + border-inline-start: 1px solid rgba( 134, 134, 134, .333 ); +} + +.topbar-button > button:disabled i, +.topbar-button > span i { + color: rgba( 134, 134, 134, .333 ); +} +.topbar-button button{ + -webkit-appearance: none; + appearance: none; + background-color: transparent; +} + +.topbar-sidebar-divider { + border-inline-start-style: solid; + border-inline-start-width: 1px; + margin-inline-end: -1px; + width: 1px; +} +.topbar-sidebar-divider::after { + content: "\00a0"; +} + +.topbar-wrapper > .topbar-area-start { + display: flex; + flex-direction: row; + flex-shrink: 0; +} +.topbar-wrapper > .topbar-area-end { + display: flex; + flex-direction: row; + flex-shrink: 0; +} +.topbar-wrapper > .topbar-hidden { + display: none; +} + +html[dir="rtl"] .topbar-button-prev i, +html[dir="rtl"] .topbar-button-next i { + transform: scaleX(-1); +} + +.topbar-content { + top: .75rem; +} +.topbar-wrapper > .topbar-area-start .topbar-content { + inset-inline-start: 1.5rem; +} +.topbar-wrapper > .topbar-area-end .topbar-content { + inset-inline-end: 1.5rem; +} +.topbar-content .topbar-content{ + /* we don't allow flyouts in flyouts; come on, don't get funny... */ + display: none; +} + +.topbar-breadcrumbs { + flex-grow: 1; + margin: 0; + padding: 0 1rem; +} +@media screen and (max-width: 47.999rem) { + .topbar-breadcrumbs { + /* we just hide the breadcrumbs instead of display: none; + this makes sure that the breadcrumbs are still usable for + accessability */ + visibility: hidden; + } +} + +.breadcrumbs { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + white-space: nowrap; +} + +.breadcrumbs meta { + display: none; +} + +.breadcrumbs li { + display: inline-block; +} + +#R-body a[aria-disabled="true"] { + pointer-events: none; + text-decoration: none; +} + +@media screen and (max-width: 59.999rem) { + #R-sidebar { + min-width: var(--INTERNAL-MENU-WIDTH-M); + max-width: var(--INTERNAL-MENU-WIDTH-M); + width: var(--INTERNAL-MENU-WIDTH-M); + } + #R-body { + margin-inline-start: var(--INTERNAL-MENU-WIDTH-M); + min-width: calc( 100% - var(--INTERNAL-MENU-WIDTH-M) ); + max-width: calc( 100% - var(--INTERNAL-MENU-WIDTH-M) ); + width: calc( 100% - var(--INTERNAL-MENU-WIDTH-M) ); + } +} +@media screen and (max-width: 47.999rem) { + /* we don't support sidebar flyout in mobile */ + .mobile-support #R-sidebar { + inset-inline-start: calc( -1 * var(--INTERNAL-MENU-WIDTH-S) ); + min-width: var(--INTERNAL-MENU-WIDTH-S); + max-width: var(--INTERNAL-MENU-WIDTH-S); + width: var(--INTERNAL-MENU-WIDTH-S); + } + .mobile-support #navshow{ + display: inline; + } + .mobile-support #R-body { + min-width: 100%; + max-width: 100%; + width: 100%; + } + .mobile-support #R-body { + margin-inline-start: 0; + } + .mobile-support.sidebar-flyout { + overflow: hidden; + } + .mobile-support.sidebar-flyout #R-sidebar { + inset-inline-start: 0; + z-index: 90; + } + .mobile-support.sidebar-flyout #R-body { + margin-inline-start: var(--INTERNAL-MENU-WIDTH-S); + overflow: hidden; + } + .mobile-support.sidebar-flyout #R-body-overlay{ + background-color: rgba( 134, 134, 134, .5 ); + bottom: 0; + cursor: pointer; + height: 100vh; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 190; + } +} + +.copy-to-clipboard-button { + border-start-start-radius: 0; + border-start-end-radius: 2px; + border-end-end-radius: 2px; + border-end-start-radius: 0; + border-style: solid; + border-width: 1px; + cursor: pointer; + font-size: .934375rem; + line-height: 1.15; +} + +span > .copy-to-clipboard-button { + border-start-start-radius: 0; + border-start-end-radius: 2px; + border-end-end-radius: 2px; + border-end-start-radius: 0; +} + +.copy-to-clipboard-button > i { + font-size: .859625rem; +} + +/* only show copy to clipboard on hover for code blocks if configured */ +div.highlight .copy-to-clipboard-button { + display: none; +} +@media (any-hover: none) { + /* if there is at least one input device that does not support hover, we want to force the copy button */ + div.highlight .copy-to-clipboard-button { + display: block; + } +} +div.highlight:hover .copy-to-clipboard-button { + display: block; +} +.disableHoverBlockCopyToClipBoard div.highlight .copy-to-clipboard-button { + display: block; +} + +div.highlight > div table + .copy-to-clipboard-button > i, +div.highlight pre:not(.mermaid) + .copy-to-clipboard-button > i, +.copy-to-clipboard-code + .copy-to-clipboard-button > i { + padding-left: 5px; + padding-right: 5px; +} + +div.highlight > div table + .copy-to-clipboard-button, +div.highlight pre:not(.mermaid) + .copy-to-clipboard-button, +pre:not(.mermaid) > .copy-to-clipboard-button { + background-color: rgba( 160, 160, 160, .2 ); + border-radius: 2px; + border-style: solid; + border-width: 1px; + right: 4px; + padding: 5px 3px; + position: absolute; + top: 4px; +} + +.disableInlineCopyToClipboard span > code.copy-to-clipboard-code + span.copy-to-clipboard-button { + display: none; +} + +.disableInlineCopyToClipboard span > code.copy-to-clipboard-code { + border-start-end-radius: 2px; + border-end-end-radius: 2px; + border-inline-end-width: 1px; +} + +#R-homelinks { + padding: 0; +} +#R-homelinks ul { + margin: .5rem 0; +} +#R-homelinks hr { + border-bottom-style: solid; + border-bottom-width: 1px; + margin: 0 1rem 3px 1rem; +} + +option { + color: initial; +} + +.expand { + margin-bottom: 1rem; + margin-top: 1rem; + position: relative; +} + +.expand > input { + -webkit-appearance: none; + appearance: none; + cursor: pointer; +} + +.expand > label { + cursor: pointer; + display: inline; + font-weight: 300; + inset-inline-start: 0; + line-height: 1.1; + margin-top: .2rem; + position: absolute; +} + +.expand > input:active + label, +.expand > input:focus + label, +.expand > label:hover { + text-decoration: underline; +} + +.expand > label > .fas { + font-size: .8rem; + width: .6rem; +} + +.expand > .expand-content { + margin-inline-start: 1rem; + margin-top: .5rem; +} +/* closed expander */ +.expand > input + label + div { + display: none; +} + +.expand > input + label > .fa-chevron-down { + display: none; +} +.expand > input + label > .fa-chevron-right { + display: inline-block; +} + +/* open expander */ +.expand > input:checked + label + div { + display: block; +} + +.expand > input:checked + label > .fa-chevron-down { + display: inline-block; +} +.expand > input:checked + label > .fa-chevron-right { + display: none; +} + +/* adjust expander for RTL reading direction */ +html[dir="rtl"] .expand > .expand-label > i.fa-chevron-right { + transform: scaleX(-1); +} + +#R-body footer.footline{ + margin-top: 2rem; +} + +.headline i, +.footline i{ + margin-inline-start: .5rem; +} +.headline i:first-child, +.footline i:first-child{ + margin-inline-start: 0; +} + +.mermaid-container { + margin-bottom: 1.7rem; + margin-top: 1.7rem; +} + +.mermaid { + display: inline-block; + border: 1px solid transparent; + padding: .5rem .5rem 0 .5rem; + position: relative; + /* don't use display: none, as this will cause no renderinge by Mermaid */ + visibility: hidden; + width: 100%; +} +.mermaid-container.zoomable > .mermaid:hover { + border-color: rgba( 134, 134, 134, .333 ); +} +.mermaid.mermaid-render { + visibility: visible; +} + +.mermaid > svg { + /* remove inline height from generated diagram */ + height: initial !important; +} +.mermaid-container.zoomable > .mermaid > svg { + cursor: grab; +} + +.svg-reset-button { + background-color: rgba( 160, 160, 160, .2 ); + border-radius: 2px; + border-style: solid; + border-width: 1px; + cursor: pointer; + display: none; + font-size: .934375rem; + line-height: 1.15; + padding: 5px 3px; + position: absolute; + right: 4px; + top: 4px; +} +.mermaid:hover .svg-reset-button.zoomed { + display: block; +} +@media (any-hover: some) { + /* if there is at least one input device that does not support hover, we want to force the reset button if zoomed */ + .svg-reset-button.zoomed { + display: block; + } +} + +.svg-reset-button > i { + font-size: .859625rem; + padding-left: 5px; + padding-right: 5px; +} + +.mermaid-code { + display: none; +} + +.include.hide-first-heading h1:first-of-type, +.include.hide-first-heading h2:first-of-type, +.include.hide-first-heading h3:first-of-type, +.include.hide-first-heading h4:first-of-type, +.include.hide-first-heading h5:first-of-type, +.include.hide-first-heading h6:first-of-type { + display: none; +} + +.include.hide-first-heading h1 + h2:first-of-type, +.include.hide-first-heading h1 + h3:first-of-type, +.include.hide-first-heading h2 + h3:first-of-type, +.include.hide-first-heading h1 + h4:first-of-type, +.include.hide-first-heading h2 + h4:first-of-type, +.include.hide-first-heading h3 + h4:first-of-type, +.include.hide-first-heading h1 + h5:first-of-type, +.include.hide-first-heading h2 + h5:first-of-type, +.include.hide-first-heading h3 + h5:first-of-type, +.include.hide-first-heading h4 + h5:first-of-type, +.include.hide-first-heading h1 + h6:first-of-type, +.include.hide-first-heading h2 + h6:first-of-type, +.include.hide-first-heading h3 + h6:first-of-type, +.include.hide-first-heading h4 + h6:first-of-type, +.include.hide-first-heading h5 + h6:first-of-type { + display: block; +} + +/* Table of contents */ + +.topbar-flyout #R-main-overlay{ + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 3rem; + z-index: 160; +} + +.topbar-content { + border: 0 solid rgba( 134, 134, 134, .166 ); + box-shadow: 1px 2px 5px 1px rgba( 134, 134, 134, .2 ); + height: 0; + opacity: 0; + overflow: hidden; + position: absolute; + visibility: hidden; + width: 0; + z-index: 180; +} + +.topbar-button.topbar-flyout .topbar-content { + border-width: 1px; + height: auto; + opacity: 1; + visibility: visible; + width: auto; +} + +.topbar-content .topbar-content-wrapper { + background-color: rgba( 134, 134, 134, .066 ); +} + +.topbar-content-wrapper { + --ps-rail-hover-color: rgba( 176, 176, 176, .25 ); + max-height: 90vh; + overflow: hidden; + padding: .5rem 1rem; + position: relative; /* PS */ +} + +.topbar-content .topbar-button .topbar-control { + border-width: 0; + padding: 0; +} +.topbar-content .topbar-button .topbar-control { + border-width: 0; + padding: .5rem 0; +} + +#TableOfContents, +.TableOfContents { + font-size: .8125rem; +} +#TableOfContents ul, +.TableOfContents ul { + list-style: none; + margin: 0; + padding: 0 1rem; +} + +#TableOfContents > ul, +.TableOfContents > ul { + padding: 0; +} + +#TableOfContents li, +.TableOfContents li { + white-space: nowrap; +} + +#TableOfContents > ul > li > a, +.TableOfContents > ul > li > a { + font-weight: 500; +} + +.btn { + border-radius: 4px; + display: inline-block; + font-size: .9rem; + font-weight: 500; + line-height: 1.1; + margin-bottom: 0; + touch-action: manipulation; + -webkit-user-select: none; + user-select: none; +} +.btn.interactive { + cursor: pointer; +} + +.btn > span, +.btn > a { + display: block; +} + +.btn > :where(button) { + -webkit-appearance: none; + appearance: none; + border-width: 0; + margin: 0; + padding: 0; +} + +.btn > * { + background-color: transparent; + border-radius: 4px; + border-style: solid; + border-width: 1px; + padding: 6px 12px; + text-align: center; + touch-action: manipulation; + -webkit-user-select: none; + user-select: none; + white-space: nowrap; +} + +.btn > *:after { + /* avoid breakage if no content is given */ + content: "\200b" +} + +#R-body #R-body-inner .btn > *.highlight:after { + background-color: transparent; +} + +.btn.interactive > .btn-interactive:focus { + outline: none; +} + +.btn.interactive > *:hover, +.btn.interactive > *:active, +.btn.interactive > *:focus { + text-decoration: none; +} + +/* anchors */ +.anchor { + cursor: pointer; + font-size: .5em; + margin-inline-start: .66em; + margin-top: .9em; + position: absolute; + visibility: hidden; +} +@media (any-hover: none) { + /* if there is at least one input device that does not support hover, we want to force the copy button */ + .anchor { + visibility: visible; + } +} + +h2:hover .anchor, +h3:hover .anchor, +h4:hover .anchor, +h5:hover .anchor, +h6:hover .anchor { + visibility: visible; +} + +/* Redfines headers style */ + +h1 a, +h2 a, +h3 a, +h4 a, +h5 a, +h6 a { + font-weight: inherit; +} + +#R-body h1 + h2, +#R-body h1 + h3, +#R-body h1 + h4, +#R-body h1 + h5, +#R-body h1 + h6, +#R-body h2 + h3, +#R-body h2 + h4, +#R-body h2 + h5, +#R-body h2 + h6, +#R-body h3 + h4, +#R-body h3 + h5, +#R-body h3 + h6, +#R-body h4 + h5, +#R-body h4 + h6, +#R-body h5 + h6 { + margin-top: 1rem; +} + +.menu-control .control-style { + cursor: pointer; + height: 1.574em; + overflow: hidden; +} + +.menu-control i { + padding-top: .25em; +} + +.menu-control i, +.menu-control span { + cursor: pointer; + display: block; + float: left; +} +html[dir="rtl"] .menu-control i, +html[dir="rtl"] .menu-control span { + float: right; +} + +.menu-control :hover, +.menu-control i:hover, +.menu-control span:hover { + cursor: pointer; +} + +.menu-control select, +.menu-control button { + -webkit-appearance: none; + appearance: none; + height: 1.33rem; + outline: none; + width: 100%; +} +.menu-control button:active, +.menu-control button:focus, +.menu-control select:active, +.menu-control select:focus{ + outline-style: solid; +} + +.menu-control select { + background-color: transparent; + background-image: none; + border: none; + box-shadow: none; + padding-left: 0; + padding-right: 0; +} + +.menu-control option { + color: rgba( 0, 0, 0, 1 ); + padding: 0; + margin: 0; +} + +.menu-control button { + background-color: transparent; + cursor: pointer; + display: block; + text-align: start; +} + +.clear { + clear: both; +} + +.footerLangSwitch, +.footerVariantSwitch, +.footerVisitedLinks, +.footerFooter { + display: none; +} + +.showLangSwitch, +.showVariantSwitch, +.showVisitedLinks, +.showFooter { + display: block; +} + +/* clears the 'X' from Chrome's search input */ +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { display: none; } + +span.math:has(> mjx-container[display]) { + display: block; +} + +@supports selector(.math:has(> mjx-container)){ + .math{ + visibility: hidden; + } + .math:has(> mjx-container){ + visibility: visible; + } +} +.math.align-left > mjx-container{ + text-align: left !important; +} + +.math.align-center > mjx-container{ + text-align: center !important; +} + +.math.align-right > mjx-container{ + text-align: right !important; +} + +.scrollbar-measure { + /* https://davidwalsh.name/detect-scrollbar-width */ + height: 100px; + overflow: scroll; + position: absolute; + width: 100px; + top: -9999px; +} + +.a11y-only { + /* idea taken from https://www.filamentgroup.com/lab/a11y-form-labels.html */ + clip-path: polygon(0 0, 1px 0, 1px 1px, 0 1px); + overflow: hidden; + position: absolute; + height: 1px; + transform: translateY(-100%); + transition: transform .5s cubic-bezier(.18,.89,.32,1.28); + white-space: nowrap; + width: 1px; +} + +/* filament style for making action visible on focus - not adapted yet +.a11y-only:focus { + position: fixed; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; + margin: 0 0 0 -100px; + top: -.3em; + left: 50%; + text-align: center; + width: 200px; + background: rgba( 255, 255, 255, 1 ); + color: rgba( 54, 133, 18, 1 ); + padding: .8em 0 .7em; + font-size: 16px; + z-index: 5000; + text-decoration: none; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + outline: 0; + transform: translateY(0%); +} +*/ + +.mermaid-container.align-right { + text-align: right; +} + +.mermaid-container.align-center { + text-align: center; +} + +.mermaid-container.align-left { + text-align: left; +} + +.searchform { + display: flex; +} + +.searchform input { + flex: 1 0 60%; + border-radius: 4px; + border: 2px solid rgba( 134, 134, 134, .125 ); + background: rgba( 134, 134, 134, .125 ); + display: block; + margin: 0; + margin-inline-end: .5rem; +} + +.searchform input::-webkit-input-placeholder, +.searchform input::placeholder { + color: rgba( 134, 134, 134, 1 ); + opacity: .666; +} + +.searchform .btn { + display: inline-flex; +} + +.searchhint { + margin-top: 1rem; + height: 1.5rem; +} + +#R-searchresults a.autocomplete-suggestion { + display: block; + font-size: 1.3rem; + font-weight: 500; + line-height: 1.5rem; + padding: 1rem; + text-decoration: none; +} + +#R-searchresults a.autocomplete-suggestion:after { + height: 0; +} + +#R-searchresults .autocomplete-suggestion > .breadcrumbs { + font-size: .9rem; + font-weight: 400; + margin-top: .167em; + padding-left: .2em; + padding-right: .2em; +} + +#R-searchresults .autocomplete-suggestion > .context { + font-size: 1rem; + font-weight: 300; + margin-top: .66em; + padding-left: .1em; + padding-right: .1em; +} + +.badge { + border-radius: 3px; + display: inline-block; + font-size: .8rem; + font-weight: 500; + vertical-align: middle; +} + +.badge > * { + border-radius: 3px; + border-style: solid; + border-width: 1px; + display: inline-block; + padding: 0 .25rem +} + +.badge > .badge-title { + background-color: rgba( 16, 16, 16, 1 ); + border-inline-end: 0; + border-start-end-radius: 0; + border-end-end-radius: 0; + color: rgba( 240, 240, 240, 1 ); + filter: contrast(2); + opacity: .75; +} + +.badge.badge-with-title > .badge-content { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.badge-content:after { + /* avoid breakage if no content is given */ + content: "\200b"; +} + +/* task list and its checkboxes */ +article ul > li:has(> input[type="checkbox"]) { + list-style: none; + margin-inline-start: -1rem; +} + +article ul > li:has(> input[type="checkbox"])::before { + content: "\200B"; /* accessibilty for Safari https://developer.mozilla.org/en-US/docs/Web/CSS/list-style */ +} + +/* https://moderncss.dev/pure-css-custom-checkbox-style/ */ +article ul > li > input[type="checkbox"] { + -webkit-appearance: none; + appearance: none; + /* For iOS < 15 */ + border: 0.15em solid currentColor; + border-radius: 0.15em; + display: inline-grid; + font: inherit; + height: 1.15em; + margin: 0; + place-content: center; + transform: translateY(-0.075em); + width: 1.15em; +} + +article ul > li > input[type="checkbox"]::before { + box-shadow: inset 1em 1em var(--INTERNAL-PRIMARY-color); + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); + content: ""; + height: 0.65em; + transform: scale(0); + transform-origin: bottom left; + transition: 120ms transform ease-in-out; + width: 0.65em; + /* Windows High Contrast Mode fallback must be last */ + background-color: CanvasText; +} + +article ul > li > input[type="checkbox"]:checked::before { + transform: scale(1); +} + +/* CSS Lightbox https://codepen.io/gschier/pen/kyRXVx */ +.lightbox-back { + align-items: center; + background: rgba( 0, 0, 0, .8 ); + bottom: 0; + display: none; + justify-content: center; + left: 0; + position: fixed; + right: 0; + text-align: center; + top: 0; + white-space: nowrap; + z-index: 1999; +} + +.lightbox-back:target { + display: flex; +} + +.lightbox-back img { + max-height: 95%; + max-width: 95%; + overflow: auto; + padding: min(2vh, 2vw); +} + +/* basic menu list styles (non-collapsible) */ + +#R-sidebar ul > li > :is( a, span ) { + display: block; + position: relative; +} + +#R-sidebar ul.space > li > * { + padding-bottom: .125rem; + padding-top: .125rem; +} +#R-sidebar ul.space > li > ul { + padding-bottom: 0; + padding-top: 0; +} + +#R-sidebar ul.morespace > li > * { + padding-bottom: .25rem; + padding-top: .25rem; +} +#R-sidebar ul.morespace > li > ul { + padding-bottom: 0; + padding-top: 0; +} + +#R-sidebar ul.enlarge > li > :is( a, span ) { + font-size: 1.1rem; + line-height: 2rem; +} +#R-sidebar ul.enlarge > li > a > .read-icon { + margin-top: .5rem; +} +#R-sidebar ul.enlarge > li > ul > li:last-child { + padding-bottom: 1rem; +} + +#R-sidebar ul ul { + padding-inline-start: 1rem; +} + +/* collapsible menu style overrides */ + +#R-sidebar ul.collapsible-menu > li { + position: relative; +} + +#R-sidebar ul.collapsible-menu > li > input { + -webkit-appearance: none; + appearance: none; + cursor: pointer; + display: inline-block; + margin-left: 0; + margin-right: 0; + margin-top: .65rem; + position: absolute; + width: 1rem; + z-index: 1; +} +#R-sidebar ul.collapsible-menu.enlarge > li > input { + margin-top: .9rem; +} + +#R-sidebar ul.collapsible-menu > li > label { + cursor: pointer; + display: inline-block; + inset-inline-start: 0; + margin-bottom: 0; /* nucleus */ + padding-inline-start: .125rem; + position: absolute; + width: 1rem; + z-index: 2; +} +#R-sidebar ul.collapsible-menu.enlarge > li > label { + font-size: 1.1rem; + line-height: 2rem; +} + +#R-sidebar ul.collapsible-menu > li > label:after { + content: ""; + display: block; + height: 1px; + transition: width 0.5s ease; + width: 0%; +} + +#R-sidebar ul.collapsible-menu > li > label:hover:after { + width: 100%; +} + +#R-sidebar ul.collapsible-menu > li > label > .fas { + font-size: .8rem; + width: .6rem; +} + +#R-sidebar ul.collapsible-menu > li > :is( a, span ) { + display: inline-block; + width: 100%; +} + +/* menu states for not(.collapsible-menu) */ + +#R-sidebar ul ul { + display: none; +} + +#R-sidebar ul > li.parent > ul, +#R-sidebar ul > li.active > ul, +#R-sidebar ul > li.alwaysopen > ul { + display: block; +} + +/* closed menu */ + +#R-sidebar ul.collapsible-menu > li > input + label ~ ul { + display: none; +} + +#R-sidebar ul.collapsible-menu > li > input + label > .fa-chevron-down { + display: none; +} +#R-sidebar ul.collapsible-menu > li > input + label > .fa-chevron-right { + display: inline-block; +} + +/* open menu */ + +#R-sidebar ul.collapsible-menu > li > input:checked + label ~ ul { + display: block; +} + +#R-sidebar ul.collapsible-menu > li > input:checked + label > .fa-chevron-down { + display: inline-block; +} +#R-sidebar ul.collapsible-menu > li > input:checked + label > .fa-chevron-right { + display: none; +} + +/* adjust menu for RTL reading direction */ + +html[dir="rtl"] #R-sidebar ul.collapsible-menu > li > label > i.fa-chevron-right { + transform: scaleX(-1); +} + +.columnize{ + column-count: 2; +} +@media screen and (min-width: 79.25rem) { + .columnize{ + column-count: 3; + } +} + +.columnize > *{ + break-inside: avoid-column; +} + +.columnize .breadcrumbs{ + font-size: .859625rem; +} + +#R-body .tab-panel{ + margin-bottom: 1.5rem; + margin-top: 1.5rem; +} + +#R-body .tab-nav{ + display: flex; + flex-wrap: wrap; +} + +#R-body .tab-nav-title{ + font-size: .9rem; + font-weight: 400; + line-height: 1.42857143; + padding: .2rem 0; + margin-inline-start: .6rem; +} + +#R-body .tab-nav-button{ + -webkit-appearance: none; + appearance: none; + background-color: transparent; + border: 1px solid transparent; + display: block; + font-size: .9rem; + font-weight: 300; + line-height: 1.42857143; + margin-inline-start: .6rem; +} + +#R-body .tab-nav-button.active{ + border-radius: 2px 2px 0 0; + cursor: default; +} + +#R-body .tab-nav-button > .tab-nav-text{ + border-bottom-style: solid; + border-bottom-width: .15rem; + display: block; + padding: .2rem .6rem 0 .6rem; +} +/* https://stackoverflow.com/a/46452396 */ +#R-body .tab-nav-button.active > .tab-nav-text{ + border-bottom-color: transparent; + border-radius: 1px 1px 0 0; + text-shadow: -0.06ex 0 0 currentColor, 0.06ex 0 0 currentColor; +} +@supports (-webkit-text-stroke-width: 0.04ex){ + #R-body .tab-nav-button.active > .tab-nav-text{ + text-shadow: -0.03ex 0 0 currentColor, 0.03ex 0 0 currentColor; + -webkit-text-stroke-width: 0.04ex; + } +} + +#R-body .tab-content{ + border-style: solid; + border-width: 1px; + display: none; + /* if setting a border to 1px, a browser instead sets it to 1dppx which is not + usable as a unit yet, so we have to calculate it ourself */ + margin-top: calc( var(--bpx1)*-1px ); + z-index: 10; +} + +#R-body .tab-content.active{ + display: block; +} + +#R-body .tab-content-text{ + padding: 1rem; +} + +/* remove margin if only a single code block is contained in the tab (FF without :has using .codify style) */ +#R-body .tab-content.codify > .tab-content-text{ + padding: 0; +} +#R-body .tab-content-text:has(> div.highlight:only-child){ + padding: 0; +} + +/* remove border from code block if single in tab */ +#R-body .tab-content-text > div.highlight:only-child > div, +#R-body .tab-content-text > div.highlight:only-child pre:not(.mermaid), +#R-body .tab-content-text > pre:not(.mermaid).pre-code:only-child{ + border-width: 0; +} + +/* bordering the menu and topbar */ + +#R-topbar { + border-bottom-style: solid; + border-bottom-width: 1px; +} + +#R-header-topbar { + border-bottom-color: transparent; + border-bottom-style: solid; + border-bottom-width: 1px; + border-inline-end-style: solid; + border-inline-end-width: 1px; + height: 3rem; + position: absolute; + top: 0; + width: 100%; + z-index: 1; +} + +#R-header-wrapper, +#R-homelinks, +#R-content-wrapper > * { + border-inline-end-style: solid; + border-inline-end-width: 1px; +} + +#topics > ul { + margin-top: 1rem; +} + +#R-sidebar ul.collapsible-menu li.active > a{ + border-style: solid; + border-width: 1px; + padding-bottom: calc( .25rem - var(--bpx1)*1px); + padding-left: calc( 1rem - var(--bpx1)*1px); + padding-right: calc( 1rem - var(--bpx1)*1px); + padding-top: calc( .25rem - var(--bpx1)*1px); + width: calc(100% + var(--bpx1)*1px); +} + +#R-menu-footer { + padding-bottom: 1rem; +} + +#R-topics { + padding-top: 1rem; +} + +.term-list ul, +.term-list li { + list-style: none; + display: inline; + padding: 0; +} +.term-list i ~ ul > li:before{ + content: " " +} +.term-list ul > li ~ li:before { + content: " | " +} diff --git a/cloudbank/css/variables.css b/cloudbank/css/variables.css new file mode 100644 index 000000000..daa3932ba --- /dev/null +++ b/cloudbank/css/variables.css @@ -0,0 +1,116 @@ +:root { + /* initially use section background to avoid flickering on load when a non default variant is active; + this is only possible because every color variant defines this variable, otherwise we would have been lost */ + --INTERNAL-PRIMARY-color: var(--PRIMARY-color, var(--MENU-HEADER-BG-color, rgba( 0, 0, 0, 0 ))); /* not --INTERNAL-MENU-HEADER-BG-color */ + --INTERNAL-SECONDARY-color: var(--SECONDARY-color, var(--MAIN-LINK-color, rgba( 72, 106, 201, 1 ))); /* not --INTERNAL-MAIN-LINK-color */ + --INTERNAL-ACCENT-color: var(--ACCENT-color, rgba( 255, 255, 0, 1 )); + + --INTERNAL-MAIN-TOPBAR-BORDER-color: var(--MAIN-TOPBAR-BORDER-color, transparent); + --INTERNAL-MAIN-LINK-color: var(--MAIN-LINK-color, var(--SECONDARY-color, rgba( 72, 106, 201, 1 ))); /* not --INTERNAL-SECONDARY-color */ + --INTERNAL-MAIN-LINK-HOVER-color: var(--MAIN-LINK-HOVER-color, var(--INTERNAL-MAIN-LINK-color)); + --INTERNAL-MAIN-BG-color: var(--MAIN-BG-color, rgba( 255, 255, 255, 1 )); + + --INTERNAL-MAIN-TEXT-color: var(--MAIN-TEXT-color, rgba( 16, 16, 16, 1 )); + --INTERNAL-MAIN-TITLES-TEXT-color: var(--MAIN-TITLES-TEXT-color, var(--INTERNAL-MAIN-TEXT-color)); + + --INTERNAL-MAIN-TITLES-H1-color: var(--MAIN-TITLES-H1-color, var(--INTERNAL-MAIN-TEXT-color)); + --INTERNAL-MAIN-TITLES-H2-color: var(--MAIN-TITLES-H2-color, var(--INTERNAL-MAIN-TITLES-TEXT-color)); + --INTERNAL-MAIN-TITLES-H3-color: var(--MAIN-TITLES-H3-color, var(--INTERNAL-MAIN-TITLES-H2-color)); + --INTERNAL-MAIN-TITLES-H4-color: var(--MAIN-TITLES-H4-color, var(--INTERNAL-MAIN-TITLES-H3-color)); + --INTERNAL-MAIN-TITLES-H5-color: var(--MAIN-TITLES-H5-color, var(--INTERNAL-MAIN-TITLES-H4-color)); + --INTERNAL-MAIN-TITLES-H6-color: var(--MAIN-TITLES-H6-color, var(--INTERNAL-MAIN-TITLES-H5-color)); + + --INTERNAL-MAIN-font: var(--MAIN-font, "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif); + --INTERNAL-MAIN-TITLES-TEXT-font: var(--MAIN-TITLES-TEXT-font, var(--INTERNAL-MAIN-font)); + + --INTERNAL-MAIN-TITLES-H1-font: var(--MAIN-TITLES-H1-font, var(--INTERNAL-MAIN-font)); + --INTERNAL-MAIN-TITLES-H2-font: var(--MAIN-TITLES-H2-font, var(--INTERNAL-MAIN-TITLES-TEXT-font)); + --INTERNAL-MAIN-TITLES-H3-font: var(--MAIN-TITLES-H3-font, var(--INTERNAL-MAIN-TITLES-H2-font)); + --INTERNAL-MAIN-TITLES-H4-font: var(--MAIN-TITLES-H4-font, var(--INTERNAL-MAIN-TITLES-H3-font)); + --INTERNAL-MAIN-TITLES-H5-font: var(--MAIN-TITLES-H5-font, var(--INTERNAL-MAIN-TITLES-H4-font)); + --INTERNAL-MAIN-TITLES-H6-font: var(--MAIN-TITLES-H6-font, var(--INTERNAL-MAIN-TITLES-H5-font)); + + --INTERNAL-CODE-theme: var(--CODE-theme, relearn-light); + --INTERNAL-CODE-font: var(--CODE-font, "Consolas", menlo, monospace); + --INTERNAL-CODE-BLOCK-color: var(--CODE-BLOCK-color, var(--MAIN-CODE-color, rgba( 39, 40, 34, 1 ))); + --INTERNAL-CODE-BLOCK-BG-color: var(--CODE-BLOCK-BG-color, var(--MAIN-CODE-BG-color, rgba( 250, 250, 250, 1 ))); + --INTERNAL-CODE-BLOCK-BORDER-color: var(--CODE-BLOCK-BORDER-color, var(--MAIN-CODE-BG-color, var(--INTERNAL-CODE-BLOCK-BG-color))); + --INTERNAL-CODE-INLINE-color: var(--CODE-INLINE-color, rgba( 94, 94, 94, 1 )); + --INTERNAL-CODE-INLINE-BG-color: var(--CODE-INLINE-BG-color, rgba( 255, 250, 233, 1 )); + --INTERNAL-CODE-INLINE-BORDER-color: var(--CODE-INLINE-BORDER-color, rgba( 251, 240, 203, 1 )); + + --INTERNAL-BROWSER-theme: var(--BROWSER-theme, light); + --INTERNAL-MERMAID-theme: var(--CONFIG-MERMAID-theme, var(--MERMAID-theme, var(--INTERNAL-PRINT-MERMAID-theme))); + --INTERNAL-OPENAPI-theme: var(--CONFIG-OPENAPI-theme, var(--OPENAPI-theme, var(--SWAGGER-theme, var(--INTERNAL-PRINT-OPENAPI-theme)))); + --INTERNAL-OPENAPI-CODE-theme: var(--CONFIG-OPENAPI-CODE-theme, var(--OPENAPI-CODE-theme, --INTERNAL-PRINT-OPENAPI-CODE-theme)); + + --INTERNAL-TAG-BG-color: var(--TAG-BG-color, var(--INTERNAL-PRIMARY-color)); + + --INTERNAL-MENU-BORDER-color: var(--MENU-BORDER-color, transparent); + --INTERNAL-MENU-TOPBAR-BORDER-color: var(--MENU-TOPBAR-BORDER-color, var(--INTERNAL-MENU-HEADER-BG-color)); + --INTERNAL-MENU-TOPBAR-SEPARATOR-color: var(--MENU-TOPBAR-SEPARATOR-color, transparent); + --INTERNAL-MENU-HEADER-BG-color: var(--MENU-HEADER-BG-color, var(--PRIMARY-color, rgba( 0, 0, 0, 0 ))); /* not --INTERNAL-PRIMARY-color */ + --INTERNAL-MENU-HEADER-BORDER-color: var(--MENU-HEADER-BORDER-color, var(--INTERNAL-MENU-HEADER-BG-color)); + --INTERNAL-MENU-HEADER-SEPARATOR-color: var(--MENU-HEADER-SEPARATOR-color, var(--INTERNAL-MENU-HEADER-BORDER-color)); + + --INTERNAL-MENU-HOME-LINK-color: var(--MENU-HOME-LINK-color, rgba( 50, 50, 50, 1 )); + --INTERNAL-MENU-HOME-LINK-HOVER-color: var(--MENU-HOME-LINK-HOVER-color, var(--MENU-HOME-LINK-HOVERED-color, rgba( 128, 128, 128, 1 ))); + + --INTERNAL-MENU-SEARCH-color: var(--MENU-SEARCH-color, var(--MENU-SEARCH-BOX-ICONS-color, rgba( 224, 224, 224, 1 ))); + --INTERNAL-MENU-SEARCH-BG-color: var(--MENU-SEARCH-BG-color, rgba( 50, 50, 50, 1 )); + --INTERNAL-MENU-SEARCH-BORDER-color: var(--MENU-SEARCH-BORDER-color, var(--MENU-SEARCH-BOX-color, var(--INTERNAL-MENU-SEARCH-BG-color))); + + --INTERNAL-MENU-SECTIONS-ACTIVE-BG-color: var(--MENU-SECTIONS-ACTIVE-BG-color, rgba( 0, 0, 0, .166 )); + --INTERNAL-MENU-SECTIONS-BG-color: var(--MENU-SECTIONS-BG-color, rgba( 40, 40, 40, 1 )); + --INTERNAL-MENU-SECTIONS-LINK-color: var(--MENU-SECTIONS-LINK-color, rgba( 186, 186, 186, 1 )); + --INTERNAL-MENU-SECTIONS-LINK-HOVER-color: var(--MENU-SECTIONS-LINK-HOVER-color, var(--INTERNAL-MENU-SECTIONS-LINK-color)); + --INTERNAL-MENU-SECTION-ACTIVE-CATEGORY-color: var(--MENU-SECTION-ACTIVE-CATEGORY-color, rgba( 68, 68, 68, 1 )); + --INTERNAL-MENU-SECTION-ACTIVE-CATEGORY-BG-color: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color, var(--INTERNAL-MAIN-BG-color)); + --INTERNAL-MENU-SECTION-ACTIVE-CATEGORY-BORDER-color: var(--MENU-SECTION-ACTIVE-CATEGORY-BORDER-color, transparent); + + --INTERNAL-MENU-VISITED-color: var(--MENU-VISITED-color, var(--INTERNAL-SECONDARY-color)); + --INTERNAL-MENU-SECTION-SEPARATOR-color: var(--MENU-SECTION-SEPARATOR-color, var(--MENU-SECTION-HR-color, rgba( 96, 96, 96, 1 ))); + + --INTERNAL-BOX-CAPTION-color: var(--BOX-CAPTION-color, rgba( 255, 255, 255, 1 )); + --INTERNAL-BOX-BG-color: var(--BOX-BG-color, rgba( 255, 255, 255, .833 )); + --INTERNAL-BOX-TEXT-color: var(--BOX-TEXT-color, var(--INTERNAL-MAIN-TEXT-color)); + + --INTERNAL-BOX-BLUE-color: var(--BOX-BLUE-color, rgba( 48, 117, 229, 1 )); + --INTERNAL-BOX-GREEN-color: var(--BOX-GREEN-color, rgba( 42, 178, 24, 1 )); + --INTERNAL-BOX-GREY-color: var(--BOX-GREY-color, rgba( 160, 160, 160, 1 )); + --INTERNAL-BOX-ORANGE-color: var(--BOX-ORANGE-color, rgba( 237, 153, 9, 1 )); + --INTERNAL-BOX-RED-color: var(--BOX-RED-color, rgba( 224, 62, 62, 1 )); + + --INTERNAL-BOX-INFO-color: var(--BOX-INFO-color, var(--INTERNAL-BOX-BLUE-color)); + --INTERNAL-BOX-NEUTRAL-color: var(--BOX-NEUTRAL-color, var(--INTERNAL-BOX-GREY-color)); + --INTERNAL-BOX-NOTE-color: var(--BOX-NOTE-color, var(--INTERNAL-BOX-ORANGE-color)); + --INTERNAL-BOX-TIP-color: var(--BOX-TIP-color, var(--INTERNAL-BOX-GREEN-color)); + --INTERNAL-BOX-WARNING-color: var(--BOX-WARNING-color, var(--INTERNAL-BOX-RED-color)); + + --INTERNAL-BOX-BLUE-TEXT-color: var(--BOX-BLUE-TEXT-color, var(--INTERNAL-BOX-TEXT-color)); + --INTERNAL-BOX-GREEN-TEXT-color: var(--BOX-GREEN-TEXT-color, var(--INTERNAL-BOX-TEXT-color)); + --INTERNAL-BOX-GREY-TEXT-color: var(--BOX-GREY-TEXT-color, var(--INTERNAL-BOX-TEXT-color)); + --INTERNAL-BOX-ORANGE-TEXT-color: var(--BOX-ORANGE-TEXT-color, var(--INTERNAL-BOX-TEXT-color)); + --INTERNAL-BOX-RED-TEXT-color: var(--BOX-RED-TEXT-color, var(--INTERNAL-BOX-TEXT-color)); + + --INTERNAL-BOX-INFO-TEXT-color: var(--BOX-INFO-TEXT-color, var(--INTERNAL-BOX-BLUE-TEXT-color)); + --INTERNAL-BOX-NEUTRAL-TEXT-color: var(--BOX-NEUTRAL-TEXT-color, var(--INTERNAL-BOX-GREY-TEXT-color)); + --INTERNAL-BOX-NOTE-TEXT-color: var(--BOX-NOTE-TEXT-color, var(--INTERNAL-BOX-ORANGE-TEXT-color)); + --INTERNAL-BOX-TIP-TEXT-color: var(--BOX-TIP-TEXT-color, var(--INTERNAL-BOX-GREEN-TEXT-color)); + --INTERNAL-BOX-WARNING-TEXT-color: var(--BOX-WARNING-TEXT-color, var(--INTERNAL-BOX-RED-TEXT-color)); + + /* print style, values taken from relearn-light as it is used as a default print style */ + --INTERNAL-PRINT-MAIN-BG-color: var(--PRINT-MAIN-BG-color, rgba( 255, 255, 255, 1 )); + --INTERNAL-PRINT-CODE-font: var(--PRINT-CODE-font, "Consolas", menlo, monospace); + --INTERNAL-PRINT-TAG-BG-color: var(--PRINT-TAG-BG-color, rgba( 125, 201, 3, 1 )); + --INTERNAL-PRINT-MAIN-font: var(--PRINT-MAIN-font, "Work Sans", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif); + --INTERNAL-PRINT-MAIN-TEXT-color: var(--PRINT-MAIN-TEXT-color, rgba( 16, 16, 16, 1 )); + --INTERNAL-PRINT-MERMAID-theme: var(--PRINT-MERMAID-theme, default); + --INTERNAL-PRINT-OPENAPI-theme: var(--PRINT-OPENAPI-theme, var(--PRINT-SWAGGER-theme, light)); + --INTERNAL-PRINT-OPENAPI-CODE-theme: var(--PRINT-OPENAPI-CODE-theme, idea); + + --INTERNAL-MENU-WIDTH-S: var(--MENU-WIDTH-S, 14.375rem); + --INTERNAL-MENU-WIDTH-M: var(--MENU-WIDTH-M, 14.375rem); + --INTERNAL-MENU-WIDTH-L: var(--MENU-WIDTH-L, 18.75rem); + --INTERNAL-MAIN-WIDTH-MAX: var(--MAIN-WIDTH-MAX, 81.25rem); +} diff --git a/cloudbank/css/variant.css b/cloudbank/css/variant.css new file mode 100644 index 000000000..07ca8332e --- /dev/null +++ b/cloudbank/css/variant.css @@ -0,0 +1,515 @@ +@import "variables.css?1723487095"; + +html { + color-scheme: only var(--INTERNAL-BROWSER-theme); +} + +body { + background-color: var(--INTERNAL-MAIN-BG-color); + color: var(--INTERNAL-MAIN-TEXT-color); + font-family: var(--INTERNAL-MAIN-font); +} + +a, +.anchor, +.topbar-button button, +#R-searchresults .autocomplete-suggestion { + color: var(--INTERNAL-MAIN-LINK-color); +} + +a:hover, +a:active, +a:focus, +.anchor:hover, +.anchor:active, +.anchor:focus, +.topbar-button button:hover, +.topbar-button button:active, +.topbar-button button:focus{ + color: var(--INTERNAL-MAIN-LINK-HOVER-color); +} + +#R-sidebar { + background: var(--INTERNAL-MENU-SECTIONS-BG-color); +} + +#R-header-wrapper { + background-color: var(--INTERNAL-MENU-HEADER-BG-color); + color: var(--INTERNAL-MENU-SEARCH-color); +} + +.searchbox { + border-color: var(--INTERNAL-MENU-SEARCH-BORDER-color); + background-color: var(--INTERNAL-MENU-SEARCH-BG-color); +} + +#R-sidebar .searchbox > :first-child, +#R-sidebar .searchbox > :last-child { + color: var(--INTERNAL-MENU-SEARCH-color); +} + +.searchbox input::-webkit-input-placeholder, +.searchbox input::placeholder { + color: var(--INTERNAL-MENU-SEARCH-color); +} + +#R-sidebar .collapsible-menu label, +#R-sidebar .menu-control, +#R-sidebar :is( a, span ) { + color: var(--INTERNAL-MENU-SECTIONS-LINK-color); +} + +#R-sidebar select:hover, +#R-sidebar .collapsible-menu li:not(.active) > label:hover, +#R-sidebar .menu-control:hover, +#R-sidebar a:hover { + color: var(--INTERNAL-MENU-SECTIONS-LINK-HOVER-color); +} + +#R-sidebar ul.enlarge > li.parent, +#R-sidebar ul.enlarge > li.active { + background-color: var(--INTERNAL-MENU-SECTIONS-ACTIVE-BG-color); +} + +#R-sidebar li.active > label, +#R-sidebar li.active > a { + color: var(--INTERNAL-MENU-SECTION-ACTIVE-CATEGORY-color); +} + +#R-sidebar li.active > a { + background-color: var(--INTERNAL-MENU-SECTION-ACTIVE-CATEGORY-BG-color); +} + +#R-sidebar ul li > a .read-icon { + color: var(--INTERNAL-MENU-VISITED-color); +} + +#R-sidebar .nav-title { + color: var(--INTERNAL-MENU-SECTIONS-LINK-color); +} + +#R-content-wrapper hr { + border-color: var(--INTERNAL-MENU-SECTION-SEPARATOR-color); +} + +#R-footer { + color: var(--INTERNAL-MENU-SECTIONS-LINK-color); +} + +mark { + background-image: linear-gradient( + to right, + color-mix( in srgb, var(--INTERNAL-ACCENT-color) 20%, transparent ), + color-mix( in srgb, var(--INTERNAL-ACCENT-color) 90%, transparent ) 4%, + color-mix( in srgb, var(--INTERNAL-ACCENT-color) 40%, transparent ) + ); +} + +kbd { + color: var(--INTERNAL-TEXT-color); + font-family: var(--INTERNAL-CODE-font); +} + +h1 { + color: var(--INTERNAL-MAIN-TITLES-H1-color); + font-family: var(--INTERNAL-MAIN-TITLES-H1-font); +} + +h2 { + color: var(--INTERNAL-MAIN-TITLES-H2-color); + font-family: var(--INTERNAL-MAIN-TITLES-H2-font); +} + +h3, .article-subheading { + color: var(--INTERNAL-MAIN-TITLES-H3-color); + font-family: var(--INTERNAL-MAIN-TITLES-H3-font); +} + +h4 { + color: var(--INTERNAL-MAIN-TITLES-H4-color); + font-family: var(--INTERNAL-MAIN-TITLES-H4-font); +} + +h5 { + color: var(--INTERNAL-MAIN-TITLES-H5-color); + font-family: var(--INTERNAL-MAIN-TITLES-H5-font); +} + +h6 { + color: var(--INTERNAL-MAIN-TITLES-H6-color); + font-family: var(--INTERNAL-MAIN-TITLES-H6-font); +} + +div.box { + background-color: var(--VARIABLE-BOX-color); + border-color: var(--VARIABLE-BOX-color); +} + +div.box > .box-label { + color: var(--VARIABLE-BOX-CAPTION-color); +} + +div.box > .box-content { + background-color: var(--VARIABLE-BOX-BG-color); + color: var(--VARIABLE-BOX-TEXT-color); +} + +.cstyle.info { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-INFO-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-INFO-TEXT-color); +} + +.cstyle.warning { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-WARNING-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-WARNING-TEXT-color); +} + +.cstyle.note { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-NOTE-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-NOTE-TEXT-color); +} + +.cstyle.tip { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-TIP-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-TIP-TEXT-color); +} + +.cstyle.primary { + --VARIABLE-BOX-color: var(--INTERNAL-PRIMARY-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-MAIN-TEXT-color); +} + +.cstyle.secondary { + --VARIABLE-BOX-color: var(--INTERNAL-SECONDARY-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-MAIN-TEXT-color); +} + +.cstyle.accent { + --VARIABLE-BOX-color: var(--INTERNAL-ACCENT-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-MAIN-TEXT-color); +} + +.cstyle.blue { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-BLUE-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-BLUE-TEXT-color); +} + +.cstyle.green { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-GREEN-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-GREEN-TEXT-color); +} + +.cstyle.grey { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-GREY-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-GREY-TEXT-color); +} + +.cstyle.orange { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-ORANGE-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-ORANGE-TEXT-color); +} + +.cstyle.red { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-RED-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-BOX-RED-TEXT-color); +} + +.cstyle.code { + --VARIABLE-BOX-color: var(--INTERNAL-CODE-BLOCK-BORDER-color); + --VARIABLE-BOX-CAPTION-color: var(--INTERNAL-CODE-BLOCK-color); + --VARIABLE-BOX-BG-color: var(--INTERNAL-CODE-BLOCK-BG-color); + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-CODE-BLOCK-color); +} + +.cstyle.transparent { + --VARIABLE-BOX-color: transparent; + --VARIABLE-BOX-CAPTION-color: var(--INTERNAL-MAIN-TITLES-TEXT-color); + --VARIABLE-BOX-BG-color: transparent; + --VARIABLE-BOX-TEXT-color: var(--INTERNAL-MAIN-TEXT-color); +} + +code, +kbd, +pre:not(.mermaid), +samp { + font-family: var(--INTERNAL-CODE-font); +} + +code { + background-color: var(--INTERNAL-CODE-INLINE-BG-color); + border-color: var(--INTERNAL-CODE-INLINE-BORDER-color); + color: var(--INTERNAL-CODE-INLINE-color); +} + +pre:not(.mermaid) { + background-color: var(--INTERNAL-CODE-BLOCK-BG-color); + border-color: var(--INTERNAL-CODE-BLOCK-BORDER-color); + color: var(--INTERNAL-CODE-BLOCK-color); +} + +div.highlight > div { + background-color: var(--INTERNAL-CODE-BLOCK-BG-color); + border-color: var(--INTERNAL-CODE-BLOCK-BORDER-color); +} + +table { + background-color: var(--INTERNAL-MAIN-BG-color); +} + +.lightbox-back img{ + background-color: var(--INTERNAL-MAIN-BG-color); +} + +#R-topbar { + background-color: var(--INTERNAL-MAIN-BG-color); +} + +.topbar-sidebar-divider { + border-inline-start-color: var(--INTERNAL-MENU-TOPBAR-SEPARATOR-color); +} +@media screen and (max-width: 47.999rem) { + .topbar-sidebar-divider { + border-inline-start-color: transparent; + } +} + +#R-body a[aria-disabled="true"], +#R-searchresults .autocomplete-suggestion > .context { + color: var(--INTERNAL-MAIN-TEXT-color); +} + +#R-searchresults .autocomplete-suggestion > .breadcrumbs { + color: var(--INTERNAL-PRIMARY-color); +} + +.copy-to-clipboard-button { + background-color: var(--INTERNAL-CODE-INLINE-BG-color); + border-color: var(--INTERNAL-CODE-INLINE-BORDER-color); + color: var(--INTERNAL-CODE-INLINE-color); + font-family: var(--INTERNAL-CODE-font); +} + +.copy-to-clipboard-button:hover { + background-color: var(--INTERNAL-CODE-INLINE-color); + color: var(--INTERNAL-CODE-INLINE-BG-color); +} + +div.highlight > div table + .copy-to-clipboard-button, +div.highlight pre:not(.mermaid) + .copy-to-clipboard-button, +pre:not(.mermaid) .copy-to-clipboard-button { + border-color: transparent; + color: var(--INTERNAL-MAIN-LINK-color); +} + +div.highlight > div table + .copy-to-clipboard-button:hover, +div.highlight pre:not(.mermaid) + .copy-to-clipboard-button:hover, +pre:not(.mermaid) .copy-to-clipboard-button:hover { + background-color: var(--INTERNAL-MAIN-LINK-color); + border-color: var(--INTERNAL-MAIN-LINK-color); + color: var(--INTERNAL-CODE-BLOCK-BG-color); +} + +.expand > label { + color: var(--INTERNAL-MAIN-LINK-color); +} + +.expand > label:hover, +.expand > label:active, +.expand > label:focus, +.expand > input:hover + label, +.expand > input:active + label, +.expand > input:focus + label{ + color: var(--INTERNAL-MAIN-LINK-HOVER-color); +} + +.svg-reset-button { + border-color: transparent; + color: var(--INTERNAL-MAIN-LINK-color); +} +.svg-reset-button:hover { + background-color: var(--INTERNAL-MAIN-LINK-color); + border-color: var(--INTERNAL-MAIN-LINK-color); + color: var(--INTERNAL-MAIN-BG-color); +} + +#R-homelinks { + background-color: var(--INTERNAL-MENU-HEADER-BORDER-color); +} + +#R-homelinks a { + color: var(--INTERNAL-MENU-HOME-LINK-color); +} + +#R-homelinks a:hover { + color: var(--INTERNAL-MENU-HOME-LINK-HOVER-color); +} + +#R-homelinks hr { + border-color: var(--INTERNAL-MENU-HEADER-SEPARATOR-color); +} + +.topbar-content { + background-color: var(--INTERNAL-MAIN-BG-color); +} + +.btn { + background-color: var(--VARIABLE-BOX-color); +} + +.btn > * { + border-color: var(--VARIABLE-BOX-color); + color: var(--VARIABLE-BOX-CAPTION-color); +} + +.btn.interactive > *:hover, +.btn.interactive > *:active, +.btn.interactive > *:focus { + background-color: var(--VARIABLE-BOX-BG-color); + color: var(--VARIABLE-BOX-TEXT-color); +} + +.btn.cstyle.transparent { + --VARIABLE-BOX-BG-color: var(--INTERNAL-BOX-BG-color); +} + +.btn.cstyle.interactive.transparent:hover, +.btn.cstyle.interactive.transparent:focus, +.btn.cstyle.interactive.transparent:active, +.btn.cstyle.interactive.transparent:has(a:hover), +.btn.cstyle.interactive.transparent:has(a:focus), +.btn.cstyle.interactive.transparent:has(a:active) { + background-color: var(--INTERNAL-BOX-NEUTRAL-color); +} + +.btn.cstyle.transparent > * { + --VARIABLE-BOX-color: var(--INTERNAL-BOX-NEUTRAL-color); + --VARIABLE-BOX-TEXT-color: var(--VARIABLE-BOX-CAPTION-color); +} + +#R-body .tags { + --VARIABLE-TAGS-color: var(--INTERNAL-MAIN-BG-color); + --VARIABLE-TAGS-BG-color: var(--VARIABLE-BOX-color); +} + +#R-body .tags a.term-link { + background-color: var(--VARIABLE-TAGS-BG-color); + color: var(--VARIABLE-TAGS-color); +} + +#R-body .tags a.term-link:before { + border-right-color: var(--VARIABLE-TAGS-BG-color); +} + +#R-body .tags a.term-link:after { + background-color: var(--VARIABLE-TAGS-color); +} + +.badge > * { + border-color: var(--VARIABLE-BOX-TEXT-color); +} + +.badge > .badge-content { + background-color: var(--VARIABLE-BOX-color); + color: var(--VARIABLE-BOX-CAPTION-color); +} + +.badge.cstyle.transparent{ + --VARIABLE-BOX-BG-color: var(--INTERNAL-BOX-BG-color); +} + +article ul > li > input[type="checkbox"] { + background-color: var(--INTERNAL-MAIN-BG-color); /* box background */ + color: var(--INTERNAL-MAIN-TEXT-color); +} + +#R-body .tab-nav-button { + color: var(--INTERNAL-MAIN-LINK-color); +} +#R-body .tab-nav-button:not(.active):hover, +#R-body .tab-nav-button:not(.active):active, +#R-body .tab-nav-button:not(.active):focus { + color: var(--INTERNAL-MAIN-LINK-HOVER-color); +} + +#R-body .tab-nav-button.active { + background-color: var(--VARIABLE-BOX-color); + border-bottom-color: var(--VARIABLE-BOX-BG-color); + color: var(--VARIABLE-BOX-TEXT-color); +} + +#R-body .tab-nav-button > .tab-nav-text{ + border-bottom-color: var(--VARIABLE-BOX-color); +} +#R-body .tab-nav-button.active > .tab-nav-text{ + background-color: var(--VARIABLE-BOX-BG-color); +} +#R-body .tab-nav-button:not(.active):hover > .tab-nav-text, +#R-body .tab-nav-button:not(.active):active > .tab-nav-text, +#R-body .tab-nav-button:not(.active):focus > .tab-nav-text { + border-bottom-color: var(--INTERNAL-MAIN-LINK-HOVER-color); +} + +#R-body .tab-content{ + background-color: var(--VARIABLE-BOX-color); + border-color: var(--VARIABLE-BOX-color); +} + +#R-body .tab-content-text{ + background-color: var(--VARIABLE-BOX-BG-color); + color: var(--VARIABLE-BOX-TEXT-color); +} + +.tab-panel-style.cstyle.initial, +.tab-panel-style.cstyle.default { + --VARIABLE-BOX-BG-color: var(--INTERNAL-MAIN-BG-color); +} + +.tab-panel-style.cstyle.transparent { + --VARIABLE-BOX-color: rgba( 134, 134, 134, .4 ); + --VARIABLE-BOX-BG-color: transparent; +} + +#R-body .tab-panel-style.cstyle.initial.tab-nav-button.active, +#R-body .tab-panel-style.cstyle.default.tab-nav-button.active, +#R-body .tab-panel-style.cstyle.transparent.tab-nav-button.active{ + background-color: var(--VARIABLE-BOX-BG-color); + border-left-color: var(--VARIABLE-BOX-color); + border-right-color: var(--VARIABLE-BOX-color); + border-top-color: var(--VARIABLE-BOX-color); +} + +#R-body .tab-panel-style.cstyle.code.tab-nav-button:not(.active){ + --VARIABLE-BOX-color: var(--INTERNAL-BOX-NEUTRAL-color); +} + +#R-body .tab-panel-style.cstyle.initial.tab-content, +#R-body .tab-panel-style.cstyle.default.tab-content, +#R-body .tab-panel-style.cstyle.transparent.tab-content{ + background-color: var(--VARIABLE-BOX-BG-color); +} + +#R-topbar { + border-bottom-color: var(--INTERNAL-MAIN-TOPBAR-BORDER-color); +} + +#R-header-topbar { + border-inline-end-color: var(--INTERNAL-MENU-TOPBAR-BORDER-color); +} +@media screen and (max-width: 47.999rem) { + .mobile-support #R-header-topbar { + border-inline-end-color: var(--INTERNAL-MENU-BORDER-color); + } +} + +#R-header-wrapper, +#R-homelinks, +#R-content-wrapper > * { + border-inline-end-color: var(--INTERNAL-MENU-BORDER-color); +} + +#R-sidebar ul.collapsible-menu li.active > a{ + border-bottom-color: var(--INTERNAL-MENU-BORDER-color); + border-top-color: var(--INTERNAL-MENU-BORDER-color); + border-inline-start-color: var(--INTERNAL-MENU-BORDER-color); + border-inline-end-color: var(--INTERNAL-MENU-SECTION-ACTIVE-CATEGORY-BORDER-color); +} diff --git a/cloudbank/deploy-cli/build/index.html b/cloudbank/deploy-cli/build/index.html new file mode 100644 index 000000000..22954cacb --- /dev/null +++ b/cloudbank/deploy-cli/build/index.html @@ -0,0 +1,311 @@ + + +
+ + + + + + + + + + + + + + + + + + + +Create application JAR files
+In the directory (root) where you cloned (or unzipped) the application and build the application JARs using the following command:
+mvn clean package
The output should be similar to this:
+[INFO] ------------------------------------------------------------------------
+[INFO] Reactor Summary for CloudBank 0.0.1-SNAPSHOT:
+[INFO]
+[INFO] CloudBank .......................................... SUCCESS [ 0.916 s]
+[INFO] account ............................................ SUCCESS [ 2.900 s]
+[INFO] checks ............................................. SUCCESS [ 1.127 s]
+[INFO] customer ........................................... SUCCESS [ 1.106 s]
+[INFO] creditscore ........................................ SUCCESS [ 0.908 s]
+[INFO] transfer ........................................... SUCCESS [ 0.455 s]
+[INFO] testrunner ......................................... SUCCESS [ 0.942 s]
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 9.700 s
+[INFO] Finished at: 2024-01-18T15:52:56-06:00
+[INFO] ------------------------------------------------------------------------
To be able to access the CLoudBank services from the public internet we need expose the services via the Apache APISIX gateway. We’re going to do that using scripts.
+Get APISIX Gateway Admin Key
+You are going to need the Admin Key for the APISIX Gateway to configure the route. It is stored in a k8s ConfigMap. Run the command and make a note of the admin key.
+kubectl -n apisix get configmap apisix -o yaml
Look for the key:
information in the admin_key
section and save it. You’ll be needing it later in this module.
admin_key:
+ # admin: can everything for configuration data
+ - name: "admin"
+ key: edd1c9f03...........
+ role: admin
Create tunnel to APISIX
+kubectl port-forward -n apisix svc/apisix-admin 9180
Create the routes
+In the root
directory run the following command. NOTE, you must add your API-KEY to the command:
(cd apisix-routes; source ./create-all-routes.sh <YOUR-API-KEY>)
The script will create the following routes:
+CloudBank Service | +URI | +
---|---|
ACCOUNT | +/api/v1/account* | +
CREDITSCORE | +/api/v1/creditscore* | +
CUSTOMER | +/api/v1/customer* | +
TESTRUNNER | +/api/v1/testrunner* | +
TRANSFER | +/transfer* | +
Verify the routes in the APISIX Dashboard
+Get the password for the APISIX dashboard.
+Retrieve the password for the APISIX dashboard using this command:
+kubectl get secret apisix-dashboard -n apisix -o jsonpath='{.data.conf\.yaml}' | base64 --decode
Start the tunnel in a new terminal window using this command.
+$ kubectl -n apisix port-forward svc/apisix-dashboard 7070:80
+Forwarding from 127.0.0.1:7070 -> 9000
+Forwarding from [::1]:7070 -> 9000
Open a web browser to http://localhost:7070 to view the APISIX Dashboard web user interface. It will appear similar to the image below.
+If prompted to login, login with username admin
and the password you got from the k8s secret earlier. Note that Oracle strongly recommends that you change the password, even though this interface is not accessible outside the cluster without a tunnel.
Click the routes menu item to see the routes you created in step three.
+ +Verify that you have three routes created
+ +Obtain the obaas-admin
password.
Execute the following command to get the obaas-admin
password:
kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d
Start a tunnel to the backend service.
+The Oracle Backend for Spring Boot and Microservices admin service is not exposed outside the Kubernetes cluster by default. Use kubectl to start a port forwarding tunnel to establish a secure connection to the admin service.
+Start a tunnel using this command:
+$ kubectl -n obaas-admin port-forward svc/obaas-admin 8080:8080
+Forwarding from 127.0.0.1:8080 -> 8080
+Forwarding from [::1]:8080 -> 8080
Start the Oracle Backend for Spring Boot and Microservices CLI oractl
+Open a new terminal Window or Tab and start the Oracle Backend for Spring Boot and Microservices CLI (oractl) using this command:
+$ oractl
+ _ _ __ _ ___
+/ \ |_) _. _. (_ / | |
+\_/ |_) (_| (_| __) \_ |_ _|_
+========================================================================================
+ Application Name: Oracle Backend Platform :: Command Line Interface
+ Application Version: (1.2.0)
+ :: Spring Boot (v3.3.0) ::
+
+ Ask for help:
+ - Slack: https://oracledevs.slack.com/archives/C03ALDSV272
+ - email: obaas_ww@oracle.com
+
+oractl:>
Connect to the Oracle Backend for Spring Boot and Microservices admin service called obaas-admin
+Connect to the Oracle Backend for Spring Boot and Microservices admin service using this command. Use thr password you obtained is Step 1.
+oractl> connect
+username: obaas-admin
+password: **************
+Credentials successfully authenticated! obaas-admin -> welcome to OBaaS CLI.
+oractl:>
Deploy CloudBank
+CloudBank will be deployed using a script into the namespace application
. The script does the following:
bind
command for the services that requires database access.++What happens when you use the oractl CLI bind command? When you run the
+bind
command, the oractl tool does several things for you:
++What happens when you use the Oracle Backend for Spring Boot and Microservices CLI (oractl) deploy command? When you run the
+deploy
command, oractl does several things for you:
The services are using Liquibase. Liquibase is an open-source database schema change management solution which enables you to manage revisions of your database changes easily. When the service gets deployed the tables
and sample data
will be created and inserted by Liquibase. The SQL executed can be found in the source code directories of CloudBank.
Run the following command to deploy CloudBank. When asked for Database/Service Password:
enter the password Welcome1234##
. You need to do this multiple times. NOTE: The deployment of CloudBank will take a few minutes.
oractl:>script --file deploy-cmds/deploy-cb-java21.txt
The output should look similar to this:
+Database/Service Password: *************
+Schema {account} was successfully Created and Kubernetes Secret {application/account} was successfully Created.
+Database/Service Password: *************
+Schema {account} was successfully Not_Modified and Kubernetes Secret {application/checks} was successfully Created.
+Database/Service Password: *************
+Schema {customer} was successfully Created and Kubernetes Secret {application/customer} was successfully Created.
+Database/Service Password: *************
+Schema {account} was successfully Not_Modified and Kubernetes Secret {application/testrunner} was successfully Created.
+uploading: account/target/account-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed.
+NOTICE: service not accessible outside K8S
+uploading: checks/target/checks-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed.
+NOTICE: service not accessible outside K8S
+uploading: customer/target/customer-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed.
+NOTICE: service not accessible outside K8S
+uploading: creditscore/target/creditscore-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed.
+NOTICE: service not accessible outside K8S
+uploading: testrunner/target/testrunner-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed.
+NOTICE: service not accessible outside K8S
+uploading: transfer/target/transfer-0.0.1-SNAPSHOT.jar
+building and pushing image...
+
+creating deployment and service...
+obaas-cli [deploy]: Application was successfully deployed.
+NOTICE: service not accessible outside K8S
Download a copy of the CloudBank sample application.
+Clone the source repository
+Create a local clone of the CloudBank source repository using this command.
+git clone -b OBAAS-1.2.0 https://github.com/oracle/microservices-datadriven.git
++Note: If you do not have git installed on your machine, you can download a zip file of the source code from GitHub and unzip it on your machine instead.
+
The source code for the CloudBank application will be in the microservices-datadriven
directory you just created, in the cloudbank-v32
subdirectory.
cd microservices-datadriven/cloudbank-v32
This directory will be referred to as the root
directory for CloudBank in this module.
This is a new chapter.
+ + +Now that you know how to build a Spring Boot microservice and deploy it to the Oracle Backend for Spring Boot and Microservices, this module will guide you through deploying all the CloudBank services and exploring the runtime and management capabilities of the platform. NOTE: The full CloudBank leverages more features than you have built so far such as monitoring, tracing etc. You will see those features in the module “Explore The Backend Platform”.
+Estimated module Time: 30 minutes
+Quick walk through on how to deploy full CloudBank application.
+ +In this module, you will:
+This module assumes you have:
+Verification of the services deployment
+Verify that the services are running properly by executing this command:
+kubectl get pods -n application
The output should be similar to this, all pods should have STATUS
as Running
. If not then you need to look at the logs for the pods/service to determine what is wrong for example kubectl logs -n application svc/customer
.
NAME READY STATUS RESTARTS AGE
+account-65cdc68dd7-k5ntz 1/1 Running 0 8m2s
+checks-78c988bdcf-n59qz 1/1 Running 0 42m
+creditscore-7b89d567cd-nm4p6 1/1 Running 0 38m
+customer-6f4dc67985-nf5kz 1/1 Running 0 41s
+testrunner-78d679575f-ch4k7 1/1 Running 0 33m
+transfer-869d796755-gn9lf 1/1 Running 0 27m
Verify the all the Cloud Bank services deployed
+In the next few commands, you need to provide the correct IP address for the API Gateway in your backend environment. You can find the IP address using this command, you need the one listed in the EXTERNAL-IP
column. In the example below the IP address is 100.20.30.40
Get external IP Address
+You can find the IP address using this command, you need the one listed in the EXTERNAL-IP
column. In the example below the IP address is 100.20.30.40
$ kubectl -n ingress-nginx get service ingress-nginx-controller
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ingress-nginx-controller LoadBalancer 10.123.10.127 100.20.30.40 80:30389/TCP,443:30458/TCP 13d
Test the create account REST endpoint with this command, use the external IP address for your API Gateway. Make a note of the accountID
in the output:
$ curl -i -X POST \
+-H 'Content-Type: application/json' \
+-d '{"accountName": "Sanjay''s Savings", "accountType": "SA", "accountCustomerId": "bkzLp8cozi", "accountOtherDetails": "Savings Account"}' \
+http://API-ADDRESS-OF-API-GW/api/v1/account
+HTTP/1.1 201
+Date: Wed, 01 Mar 2023 18:35:31 GMT
+Content-Type: application/json
+Transfer-Encoding: chunked
+Connection: keep-alive
+
+{"accountId":24,"accountName":"Sanjays Savings","accountType":"SA","accountCustomerId":"bkzLp8cozi","accountOpenedDate":null,"accountOtherDetails":"Savings Account","accountBalance":0}
Test the get account REST endpoint with this command, use the IP address for your API Gateway and the accountId
that was returned in the previous command:
curl -s http://API-ADDRESS-OF-API-GW/api/v1/account/<accountId> | jq .
Output should be similar to this:
+{
+ "accountId": 24,
+ "accountName": "Sanjay's Savings",
+ "accountType": "SA",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": null,
+ "accountOtherDetails": "Savings Account",
+ "accountBalance": 1040
+}
Test on of the customer REST endpoints with this command, use the IP Address for your API Gateway.
+curl -s http://API-ADDRESS-OF-API-GW/api/v1/customer | jq
Output should be similar to this:
+[
+ {
+ "customerId": "qwertysdwr",
+ "customerName": "Andy",
+ "customerEmail": "andy@andy.com",
+ "dateBecameCustomer": "2023-11-06T20:06:19.000+00:00",
+ "customerOtherDetails": "Somekind of Info",
+ "customerPassword": "SuperSecret"
+ },
+ {
+ "customerId": "aerg45sffd",
+ "customerName": "Sanjay",
+ "customerEmail": "sanjay@sanjay.com",
+ "dateBecameCustomer": "2023-11-06T20:06:19.000+00:00",
+ "customerOtherDetails": "Information",
+ "customerPassword": "Welcome"
+ },
+ {
+ "customerId": "bkzLp8cozi",
+ "customerName": "Mark",
+ "customerEmail": "mark@mark.com",
+ "dateBecameCustomer": "2023-11-06T20:06:19.000+00:00",
+ "customerOtherDetails": "Important Info",
+ "customerPassword": "Secret"
+ }
+]
Test the creditscore REST endpoint with this command
+curl -s http://API-ADDRESS-OF-API-GW/api/v1/creditscore | jq
Output should be similar to this:
+{
+ "Date": "2023-11-06",
+ "Credit Score": "686"
+}
Test the check service
+Start a tunnel to the testrunner service.
+$ kubectl -n application port-forward svc/testrunner 8084:8080
+Forwarding from 127.0.0.1:8084 -> 8080
+Forwarding from [::1]:8084 -> 8080
Deposit a check using the deposit REST endpoint
+Run this command to deposit a check, make sure you use the accountId from the account you created earlier.
+$ curl -i -X POST -H 'Content-Type: application/json' -d '{"accountId": 2, "amount": 256}' http://localhost:8084/api/v1/testrunner/deposit
+HTTP/1.1 201
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Thu, 02 Nov 2023 18:02:06 GMT
+
+{"accountId":2,"amount":256}
Check the log of the check service
+Execute this command to check the log file of the check service:
+kubectl -n application logs svc/checks
The log file should contain something similar to this (with your accountId):
+Received deposit <CheckDeposit(accountId=2, amount=256)>
Check the Journal entries using the journal REST endpoint. Replace API-ADDRESS-OF-API-GW
with your external IP Address.
curl -i http://API-ADDRESS-OF-API-GW/api/v1/account/2/journal
The output should be similar to this (with your AccountId). Note the journalId, you’re going to need it in the next step.
+HTTP/1.1 200
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Thu, 02 Nov 2023 18:06:45 GMT
+
+[{"journalId":1,"journalType":"PENDING","accountId":2,"lraId":"0","lraState":null,"journalAmount":256}]
Clearance of a check using the clear REST endpoint using your journalId:
+curl -i -X POST -H 'Content-Type: application/json' -d '{"journalId": 1}' http://localhost:8084/api/v1/testrunner/clear
HTTP/1.1 201
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Thu, 02 Nov 2023 18:09:17 GMT
+
+{"journalId":1}
Check the logs of the checks service
+Execute this command to check the log file of the check service:
+kubectl -n application logs svc/checks
The log file should contain something similar to this (with your journalId):
+Received clearance <Clearance(journalId=1)>
Check the journal REST endpoint
+Execute this command to check the Journal. Replace API-ADDRESS-OF-API-GW
with your External IP Address and ACCOUNT-ID
with your account id.
curl -i http://API-ADDRESS-OF-API-GW/api/v1/account/ACCOUNT-ID/journal
The output should look like this (with your accountId):
+`HTTP/1.1 200
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Thu, 02 Nov 2023 18:36:31 GMT
+
+[{"journalId":1,"journalType":"DEPOSIT","accountId":2,"lraId":"0","lraState":null,"journalAmount":256}]`
Test Saga transactions across Microservices using the transfer service.
+Start a tunnel to the testrunner service.
+$ kubectl -n application port-forward svc/transfer 8085:8080
+Forwarding from 127.0.0.1:8085 -> 8080
+Forwarding from [::1]:8085 -> 8080
Check the account balances for two accounts, in this example the account numbers are 1 and 2. Replace API-ADDRESS-OF-API-GW
with your External IP Address
curl -s http://API-ADDRESS-OF-API-GW/api/v1/account/1 | jq ; curl -s http://API-ADDRESS-OF-API-GW/api/v1/account/2 | jq
The output should be similar to this. Make a note of the accountBalance
values.
{
+ "accountId": 1,
+ "accountName": "Andy's checking",
+ "accountType": "CH",
+ "accountCustomerId": "qwertysdwr",
+ "accountOpenedDate": "2023-11-06T19:58:58.000+00:00",
+ "accountOtherDetails": "Account Info",
+ "accountBalance": -20
+ }
+ {
+ "accountId": 2,
+ "accountName": "Mark's CCard",
+ "accountType": "CC",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": "2023-11-06T19:58:58.000+00:00",
+ "accountOtherDetails": "Mastercard account",
+ "accountBalance": 1000
+ }
Perform a transfer between the accounts (transfer $100 from account 2 to account 1)
+$ curl -X POST "http://localhost:8085/transfer?fromAccount=2&toAccount=1&amount=100"
+transfer status:withdraw succeeded deposit succeeded
Check that the transfer has been made.
+curl -s http://API-ADDRESS-OF-API-GW/api/v1/account/1 | jq ; curl -s http://API-ADDRESS-OF-API-GW/api/v1/account/2 | jq
The output should be similar to this. Make a note of the accountBalance
values.
{
+ "accountId": 1,
+ "accountName": "Andy's checking",
+ "accountType": "CH",
+ "accountCustomerId": "qwertysdwr",
+ "accountOpenedDate": "2023-11-06T19:58:58.000+00:00",
+ "accountOtherDetails": "Account Info",
+ "accountBalance": 80
+ }
+ {
+ "accountId": 2,
+ "accountName": "Mark's CCard",
+ "accountType": "CC",
+ "accountCustomerId": "bkzLp8cozi",
+ "accountOpenedDate": "2023-11-06T19:58:58.000+00:00",
+ "accountOtherDetails": "Mastercard account",
+ "accountBalance": 900
+ }
This concludes the module Deploy the full CloudBank Application using the oractl
CLI interface.
Create application JAR files
+In the directory where you cloned (or unzipped) the application and build the application JARs using the following command:
+<copy>mvn clean package</copy>
The output should be similar to this:
+[INFO] ------------------------------------------------------------------------
+[INFO] Reactor Summary for cloudbank 0.0.1-SNAPSHOT:
+[INFO]
+[INFO] cloudbank .......................................... SUCCESS [ 0.972 s]
+[INFO] account ............................................ SUCCESS [ 2.877 s]
+[INFO] customer ........................................... SUCCESS [ 1.064 s]
+[INFO] creditscore ........................................ SUCCESS [ 0.922 s]
+[INFO] transfer ........................................... SUCCESS [ 0.465 s]
+[INFO] testrunner ......................................... SUCCESS [ 0.931 s]
+[INFO] checks ............................................. SUCCESS [ 0.948 s]
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 8.480 s
+[INFO] Finished at: 2023-11-06T12:35:17-06:00
+[INFO] ------------------------------------------------------------------------
Download a copy of the CloudBank sample application.
+Clone the source repository
+Create a local clone of the CloudBank source repository using this command.
+<copy>git clone -b OBAAS-1.2.0 https://github.com/oracle/microservices-datadriven.git</copy>
++Note: If you do not have git installed on your machine, you can download a zip file of the source code from GitHub and unzip it on your machine instead.
+
The source code for the CloudBank application will be in the microservices-datadriven
directory you just created, in the cloudbank-v32
subdirectory.
<copy>cd microservices-datadriven/cloudbank-v32</copy>
This is a new chapter.
+ + +Now that you know how to build a Spring Boot microservice and deploy it to the Oracle Backend for Spring Boot and Microservices, this module will guide you through deploying the rest of the Cloud Bank services that we have already built for you and exploring the runtime and management capabilities of the platform.
+If you already have completed the module Deploy the full CloudBank Application using the CLI You can skip Task 1 and Task 2.
+Estimated module Time: 30 minutes
+In this module, you will:
+This module assumes you have:
+If you have done the optional Task 11 of Lab. 2, you could proceed doing the activities from Task 3 to Task 5 using Oracle Backend for Spring Boot VS Code plugin. +If you don’t see the plugin in the left bar, with the Oracle logo, as shown here:
+ +click on Additional Views menu to select the Oracle Backend fo Spring Boot and Microservices.
+The Oracle Backend fo Spring Boot and Microservices VS Code plugin will ask to specify the Kubernetes config file full path as shown here:
+ +By default, it’s shown the path in the user’s Home directory .kube/config in which normally kubectl stores all the information regarding the K8S clusters configured. You could set the full path of another Kubernetes config file. +If the file is correctly loaded, the plugin will show the list of contexts available in which select one:
+ +In positive case, you should see a tree view with one node and the context chosen:
+ +If the file path it hasn’t been correctly set, it will show an error message:
+ +To restart the plugin and proceed again in Kubernetes config file setting, in command palette execute a Reload Window command:
+ +How to access to cluster
+Until you create a dedicated ssh tunnel to the Kubernetes cluster, and you don’t connect to Oracle Backend for Spring Boot admin services, you will not be able to browse resources included into the Oracle Backend for Spring Boot deployment. To do this, follow these steps:
+Obtain the obaas-admin
password by executing the following command in a terminal window to get the obaas-admin
password:
$ <copy>kubectl get secret -n azn-server oractl-passwords -o jsonpath='{.data.admin}' | base64 -d</copy>
Right-click on the cluster name and select Set UID/PWD:
+ +Enter the username obaas-admin for the Oracle Backend for Spring Boot.
+ +Followed by the password you obtained in an earlier step:
+ +Two message boxes will confirm credentials have been set correctly:
+ +WARNING: if you don’t execute this steps and try to expand the kubernetes context, you will receive a message:
+ +Select again the cluster and click the right mouse button and choose Create Admin tunnel menu item.
+ +VS Code will open a new terminal that will try to open a tunnel to the Kubernetes cluster on a local port, starting from 8081:
+ +Before proceed to connection, please wait until the tunnel is established and the terminal shows a message like this:
+ +NOTE: if the K8s cluster it’s not related to an Oracle Backend for Spring Boot deployment, the tunnel creation will fail. In this case in command palette execute a window reload too chose another cluster. If you have any problem in connection, you could start another tunnel: the plugin will try on another local port to connect to the cluster.
+Again select the cluster and by clicking the right mouse button choose Connect menu item. This will create a session with credentials set at the first step.
+ +Explore resources
+As soon as completed the steps to create tunnel, and you get connected to the backend, it’s possible to expand or refresh the tree related to the deployment.
+ +You’ll see four top classes of resources that can be exploded in underlying items:
+Let’s go to show the operations you can do on each item of browse tree.
+Open the list clicking on the arrow at the left of applications, and then expand the application about you want to know which services includes:
+ +it should be empty. If not, proceed to delete the full application and re-create it through the plug-in:
+First, select the default application and with right-click on mouse, select Delete application:
+ +Wait a moment and refresh the content of applications leaf. When empty, select applications and with right-click on mouse, select Add application:
+ +Fill in the command palette the (application name) with application:
+ +The four Spring Boot microservices deployment
+First it must be bind the service if the case. For account service you have to:
+Select applications leaf and with right click select Bind a service item menu:
+ +and the input following values:
+you’ll get the message:
+Repeat the same for:
+checks service you have to:
+customer service you have to:
+testrunner service you have to:
+Ensure to get the message like this for all previous binding:
+Let’s start with the first service deployment:
+Select application under applications and Right-click on mouse to select Add service -> upload .jar:
+ +Look for the accounts-0.0.1-SNAPSHOT.jar file built previously:
+ +In the command palette will be asked all the parameters needed to upload the services:
+Service Name : account
Bind [jms] : ``
+Image Version: 0.0.1
Java Image: leave default ghcr.io/graalvm/jdk:ol7-java17-22.2.0
Add Health probe?: False
+Service Port: leave default 8080
Service Profile: leave default obaas
Initial Replicas : 1
+Inform the database name for Liquibase: admin
You will see messages that confirm the deployment is started:
+ +Finally, you’ll receive the message “Service deployed successfully”:
+ +Refreshing the application leaf, you should see now:
+ +Repeat the same for:
+checks service deployment:
+checks
0.0.1
ghcr.io/graalvm/jdk:ol7-java17-22.2.0
8080
obaas
admin
customer service deployment:
+customer
0.0.1
ghcr.io/graalvm/jdk:ol7-java17-22.2.0
8080
obaas
admin
creditscore service deployment:
+creditscore
0.0.1
ghcr.io/graalvm/jdk:ol7-java17-22.2.0
8080
obaas
testrunner service deployment:
+testrunner
0.0.1
ghcr.io/graalvm/jdk:ol7-java17-22.2.0
8080
obaas
transfer service deployment:
+transfer
0.0.1
ghcr.io/graalvm/jdk:ol7-java17-22.2.0
8080
obaas
Be sure to receive for all the deployments a message that confirms the deployment is started and finally “Service deployed successfully”.
+Now we have the three services up & running as you should see from VS Code plug-in:
+ +Verify that the services are running properly by executing this command:
+$ <copy>kubectl get all -n application</copy>
The output should be similar to this, all applications must have STATUS
as Running
(base) cdebari@cdebari-mac ~ % kubectl get all -n application
+NAME READY STATUS RESTARTS AGE
+pod/account-777c6b57dc-mgnq9 1/1 Running 0 17m
+pod/checks-65cf5f77f9-nfqt4 1/1 Running 0 15m
+pod/creditscore-648fd868ff-twjsl 1/1 Running 0 9m43s
+pod/customer-5dc57bc575-2n6mf 1/1 Running 0 13m
+pod/testrunner-7df6f8f4c5-6t6gf 1/1 Running 0 8m50s
+pod/transfer-59d9c55df5-llppn 1/1 Running 0 7m57s
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/account ClusterIP 10.96.140.242 <none> 8080/TCP 17m
+service/checks ClusterIP 10.96.61.226 <none> 8080/TCP 15m
+service/creditscore ClusterIP 10.96.97.155 <none> 8080/TCP 9m44s
+service/customer ClusterIP 10.96.118.193 <none> 8080/TCP 13m
+service/testrunner ClusterIP 10.96.235.62 <none> 8080/TCP 8m51s
+service/transfer ClusterIP 10.96.98.16 <none> 8080/TCP 7m58s
+
+NAME READY UP-TO-DATE AVAILABLE AGE
+deployment.apps/account 1/1 1 1 17m
+deployment.apps/checks 1/1 1 1 15m
+deployment.apps/creditscore 1/1 1 1 9m44s
+deployment.apps/customer 1/1 1 1 13m
+deployment.apps/testrunner 1/1 1 1 8m51s
+deployment.apps/transfer 1/1 1 1 7m58s
+
+NAME DESIRED CURRENT READY AGE
+replicaset.apps/account-777c6b57dc 1 1 1 17m
+replicaset.apps/checks-65cf5f77f9 1 1 1 15m
+replicaset.apps/creditscore-648fd868ff 1 1 1 9m44s
+replicaset.apps/customer-5dc57bc575 1 1 1 13m
+replicaset.apps/testrunner-7df6f8f4c5 1 1 1 8m51s
+replicaset.apps/transfer-59d9c55df5 1 1 1 7m58s
Expose the services using APISIX Gateway
+Execute the same actions as described in Lab. 5, Task 5 except for the step 4., that it could be executed in the following alternative way, accessing comfortably to the APISIX admin console straight from VS Code.
+Select under platformServices the leaf apisix and, with a right-click on mouse, select Open Apisix console:
+ +It will open a terminal window in which it will be started a tunneling to that service, that will end opening a message box with a button you can click to open the APISIX admin console in a new browser:
+ +The Oracle Backend for Spring Boot and Microservices includes an Oracle Database. An instance of an Oracle Autonomous Database (Shared) is created during installation.
+To access the database from a local machine you need to download the wallet and configure SQLcl
to use the downloaded wallet.
Login into the OCI Console. Oracle Cloud
+Navigate to Autonomous Transaction Processing.
+ +Make sure that you have the right compartment selected and click on the database name. The database name is composed by the application name you gave during install with the suffix of DB
. In the picture below the Application Name is CBANK
so the database name is CBANKDB
. If you didn’t provide an Application name, the database will name will be a random pet name with the suffix DB
in the compartment you deployed application.
Click Database Connection to retrieve the Wallet.
+ +Click Download Wallet to download the Wallet.
+ +You will need to provide a password for the Wallet. Make a note of where the wallet is located you’ll be needing it when connection to the Database.
+ +Close the Database Connection Dialog Box
+ +Obtain the ADMIN password
+To get the ADMIN password for the database you need to read a k8s secret. Replace the cbankdb
with the Database name for you deployment in the command below. The name is composed by the Application Name you gave during deployment with the suffix DB
. If you didn’t provide an Application name, the database will name will be a random pet name with the suffix DB
in the compartment you deployed application. Get the password using this command:
$ kubectl -n application get secret cbankdb-db-secrets -o jsonpath='{.data.db\.password}' | base64 -d
Start SQLcl
and connect to the Database:
Start SQLcl using the following command:
+$ sql /nolog
+
+
+SQLcl: Release 22.4 Production on Fri Mar 03 10:33:33 2023
+
+Copyright (c) 1982, 2023, Oracle. All rights reserved.
+
+SQL>
Run the following command to load the wallet. Make sure you use the right location and name of the wallet
+SQL> set cloudconfig /path/to/wallet/wallet_name.zip
Display the TNS Entries by executing the following command. The TNS Entries will be different for your deployment.
+SQL> show tns
+CLOUD CONFIG set to: /path/to/wallet/wallet_name.zip
+
+TNS Lookup Locations
+--------------------
+
+TNS Locations Used
+------------------
+1. /path/to/wallet/wallet_name.zip
+2. /Users/atael
+
+Available TNS Entries
+---------------------
+CBANKDB_HIGH
+CBANKDB_LOW
+CBANKDB_MEDIUM
+CBANKDB_TP
+CBANKDB_TPURGENT
+SQL>
Connect to the Database using this command. Replace the ADMIN-PASSWORD
with the password obtained from the k8s secret and replace TNS-ENTRY
with your database name followed by _TP
. In this example it would be CBANKDB_TP
SQL> connect ADMIN/ADMIN-PASSWORD@TNS-ENTRY
+Connected.
You can now close the connection or leave it open as you are going to need it in later Labs.
+If you plan to do the optional part of Lab. 5, you need to install in VS Code the Oracle Backend for Spring Boot and Microservices VS Code plugin. It is an extension to browse and deploy applications on the Oracle Backend for Spring Boot and Microservices platform. This plugin allows to inspect the content of an Oracle Backend for Spring Boot and Microservices deployment, in terms of applications, services and related configurations.
+Download the plug-in from here.
+On the VS Code right menu bar, click on Extensions item:
+ +From the up-right corner menu, choose Install from VSIX…:
+ +and upload plug-in binaries previously downloaded.
+Re-start VS Code to make fully operative the plugin, in command palette execute a window reload:
+ +If you don’t see the plugin in the left bar, with the Oracle logo, as shown here:
+ +click on Additional Views menu to select the Oracle Backend for Spring Boot and Microservices.
+Oracle recommends Visual Studio Code, which you can download here, and the following extensions to make it easier to write and build your code:
+ +++Note: It is possible to use other Integrated Development Environments however all the instructions in this Livemoduleare written for and tested with Visual Studio Code, so we recommend that you use it for this Live Lab.
+
Download and install Visual Studio Code
+Download Visual Studio Code from this website and run the installer for your operating system to install it on your machine.
+ +Install the recommended extensions
+Start Visual Studio Code, and then open the extensions tab (Ctrl-Shift-X or equivalent) and use the search bar at the top to find and install each of the extensions listed above.
+ +xyz
+ + +This module walks you through setting up your development environment to work with Oracle Backend for Spring Boot and Microservices.
+Estimated time: 20 minutes
+The following platforms are recommended for a development environment:
+The following tools are recommended for a development environment:
+If you wish to test locally or offline, the following additional tools are recommended:
+In this module, you will:
+This module assumes you have:
+Oracle recommends the Java SE Development Kit. +Themoduleis using Spring Boot 3.3.x so Java 21 is required.
+Download and install the Java Development Kit
+Download the latest x64 Java 21 Development Kit from Java SE Development Kit.
+Decompress the archive in your chosen location, e.g., your home directory and then add it to your path (the exact version of Java might differ in your environment):
+export JAVA_HOME=$HOME/jdk-21.0.3
+export PATH=$JAVA_HOME/bin:$PATH
Verify the installation
+Verify the Java Development Kit is installed with this command (the exact version of Java might differ in your environment):
+$ java -version
+java version "21.0.3" 2022-04-19 LTS
+Java(TM) SE Runtime Environment (build 21.0.3+8-LTS-111)
+Java HotSpot(TM) 64-Bit Server VM (build 21.0.3+8-LTS-111, mixed mode, sharing)
++Note: Native Images: If you want to compile your Spring Boot microservices into native images, you must use GraalVM, which can be downloaded from here.
+
At the end of the previous lab, during the verification of the installation, you looked at the end of the apply log and copied a command to obtain a Kubernetes configuration file to access your cluster. In that lab, you used OCI CLoud Shell to confirm you could access the cluster. Now, you need to configure similar access from your own development machine. You can run that same command on your local machine, we recommend that you choose a different location for the file, so it does not overwrite or interfere with any other Kubernetes configuration file you might already have on your machine.
+Create the Kubernetes configuration file
+Run the command provided at the end of your installation log to obtain the Kubernetes configuration file. The command will be similar to this:
+$ oci ce cluster create-kubeconfig --cluster-id ocid1.cluster.oc1.phx.xxxx --file path/to/kubeconfig --region us-phoenix-1 --token-version 2.0.0 --kube-endpoint PUBLIC_ENDPOINT
Configure kubectl to use the Kubernetes configuration file you just created
+Set the KUBECONFIG environment variable to point to the file you just created using this command (provide the path to where you created the file):
+$ export KUBECONFIG=/path/to/kubeconfig
Verify access to the cluster
+Check that you can access the cluster using this command:
+$ kubectl get pods -n obaas-admin
+NAME READY STATUS RESTARTS AGE
+obaas-admin-bf4cd5f55-z54pk 2/2 Running 2 (9d ago) 9d
Your output will be slightly different, but you should see one pod listed in the output. This is enough to confirm that you have correctly configured access to the Kubernetes cluster.
+In later labs, you will look various resources in the Kubernetes cluster and access some of them using port forwarding (tunneling). To do this, you will need to install kubectl on your machine, and since Oracle Container Engine for Kubernetes uses token based authentication for kubectl access, you will also need to install the OCI CLI so that kubectl can obtain the necessary token.
+Install kubectl
+Install kubectl from the Kubernetes website. Click on the link for your operating system and follow the instructions to complete the installation. As mentioned in the instructions, you can use this command to verify the installation, and you can ignore the warning since we are just checking the installation was successful (your output may be slightly different):
+$ kubectl version --client
+I0223 08:40:30.072493 26355 versioner.go:56] Remote kubernetes server unreachable
+WARNING: This version information is deprecated and will be replaced with the output from kubectl version --short. Use --output=yaml|json to get the full version.
+Client Version: version.Info{Major:"1", Minor:"24", GitVersion:"v1.24.1", GitCommit:"3ddd0f45aa91e2f30c70734b175631bec5b5825a", GitTreeState:"clean", BuildDate:"2022-05-24T12:26:19Z", GoVersion:"go1.18.2", Compiler:"gc", Platform:"linux/amd64"}
+Kustomize Version: v4.5.4
Install the OCI CLI
+Install the OCI CLI from the Quickstart documentation. Click on the link for your operating system and follow the instructions to complete the installation. After installation is complete, use this command to verify the installation (your output might be slightly different):
+$ oci --version
+3.23.2
Configure the OCI CLI
+Review the instructions in the documentation for configuring the OCI CLI. The simplest way to configure the CLI is to use the guided setup by running this command:
+$ oci setup config
This will guide you through the process of creating your configuration file. Once you are done, check that the configuration is good by running this command (note that you would have obtained the tenancy OCID during the previous step, and your output might look slightly different):
+$ oci iam tenancy get --tenancy-id ocid1.tenancy.oc1..xxxxx
+{
+ "data": {
+ "description": "mytenancy",
+ "freeform-tags": {},
+ "home-region-key": "IAD",
+ "id": "ocid1.tenancy.oc1..xxxxx",
+ "name": "mytenancy",
+ "upi-idcs-compatibility-layer-endpoint": null
+ }
+}
You can use either Maven or Gradle to build your Spring Boot applications. If you prefer Maven, follow the steps in this task. If you prefer Gradle, skip to the next task instead.
+Download Maven
+Download Maven from the Apache Maven website.
+Install Maven
+Decompress the archive in your chosen location, e.g., your home directory and then add it to your path (the exact version of maven might differ in your environment):
+$ export PATH=$HOME/apache-maven-3.8.6/bin:$PATH
Verify installation
+You can verify it is installed with this command (note that your version may give slightly different output):
+$ mvn -v
+Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
+Maven home: /home/mark/apache-maven-3.8.6
+Java version: 21.0.3, vendor: Oracle Corporation, runtime: /home/mark/jdk-21.0.3
+Default locale: en, platform encoding: UTF-8
+OS name: "linux", version: "5.10.102.1-microsoft-standard-wsl2", arch: "amd64", family: "unix"
The Oracle Backend for Spring Boot CLI (oractl) is used to configure your backend and to deploy your Spring Boot applications to the backend.
+Download the Oracle Backend for Spring Boot and Microservices CLI
+Download the CLI from here
+Install the Oracle Backend for Spring Boot and Microservices CLI
+To install the CLI, you just need to make sure it is executable and add it to your PATH environment variable.
+chmod +x oractl
+export PATH=/path/to/oractl:$PATH
NOTE: If environment is a Mac you need run the following command sudo xattr -r -d com.apple.quarantine <downloaded-file>
otherwise will you get a security warning and the CLI will not work.
Verify the installation
+Verify the CLI is installed using this command:
+ $ oractl version
+ _ _ __ _ ___
+/ \ |_) _. _. (_ / | |
+\_/ |_) (_| (_| __) \_ |_ _|_
+===================================================================
+Application Name: Oracle Backend Platform :: Command Line Interface
+Application Version: (1.2.0)
+:: Spring Boot (v3.3.0) ::
+
+Ask for help:
+ - Slack: https://oracledevs.slack.com/archives/C06L9CDGR6Z
+ - email: obaas_ww@oracle.com
+
+Build Version: 1.2.0
(Optional) Install SQLcl
+If you do not already have a database client, Oracle SQL Developer Command Line (SQLcl) is a free command line interface for Oracle Database which includes great features like auto-completion and command history. All the Labs are using SQLcl as the database client.
+If you choose to use SQLcl make sure it is in your PATH
variable:
export PATH=/path/to/sqlcl:$PATH
Welcome to CloudBank - an on-demand, self-paced learning resource you can use +to learn about developing microservices with Spring Boot +and deploying, running and managing them with Oracle Backend for Spring Boot and Microservices.
+You can follow through from beginning to end, or you can start at any module that you are interested in.
+To complete the modules you will need docker-compose
to run the backend and Oracle Database containers - you
+can use Docker Desktop, Rancher Desktop, Podman Desktop or similar.
You will need a Java SDK and either Maven or Gradle to build your applicaitons. An IDE is not strictly required, +but you will have a better overall experience if you use one. We recommend Visual Studio Code or IntelliJ.
+CloudBank contains the following modules:
+p&&(p=e.lineIndent),J(a))f++;else{if(e.lineIndent
0){for(r=a,o=0;r>0;r--)(a=ee(l=e.input.charCodeAt(++e.position)))>=0?o=(o<<4)+a:ce(e,"expected hexadecimal character");e.result+=ne(o),e.position++}else ce(e,"unknown escape sequence");n=i=e.position}else J(l)?(pe(e,n,i,!0),ye(e,ge(e,!1,t)),n=i=e.position):e.position===e.lineStart&&me(e)?ce(e,"unexpected end of the document within a double quoted scalar"):(e.position++,i=e.position)}ce(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?y=!0:!function(e){var t,n,i;if(42!==(i=e.input.charCodeAt(e.position)))return!1;for(i=e.input.charCodeAt(++e.position),t=e.position;0!==i&&!z(i)&&!X(i);)i=e.input.charCodeAt(++e.position);return e.position===t&&ce(e,"name of an alias node must contain at least one character"),n=e.input.slice(t,e.position),P.call(e.anchorMap,n)||ce(e,'unidentified alias "'+n+'"'),e.result=e.anchorMap[n],ge(e,!0,-1),!0}(e)?function(e,t,n){var i,r,o,a,l,c,s,u,p=e.kind,f=e.result;if(z(u=e.input.charCodeAt(e.position))||X(u)||35===u||38===u||42===u||33===u||124===u||62===u||39===u||34===u||37===u||64===u||96===u)return!1;if((63===u||45===u)&&(z(i=e.input.charCodeAt(e.position+1))||n&&X(i)))return!1;for(e.kind="scalar",e.result="",r=o=e.position,a=!1;0!==u;){if(58===u){if(z(i=e.input.charCodeAt(e.position+1))||n&&X(i))break}else if(35===u){if(z(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&me(e)||n&&X(u))break;if(J(u)){if(l=e.line,c=e.lineStart,s=e.lineIndent,ge(e,!1,-1),e.lineIndent>=t){a=!0,u=e.input.charCodeAt(e.position);continue}e.position=o,e.line=l,e.lineStart=c,e.lineIndent=s;break}}a&&(pe(e,r,o,!1),ye(e,e.line-l),r=o=e.position,a=!1),Q(u)||(o=e.position+1),u=e.input.charCodeAt(++e.position)}return pe(e,r,o,!1),!!e.result||(e.kind=p,e.result=f,!1)}(e,d,1===i)&&(y=!0,null===e.tag&&(e.tag="?")):(y=!0,null===e.tag&&null===e.anchor||ce(e,"alias node should not have any properties")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===g&&(y=c&&be(e,h))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&ce(e,'unacceptable node kind for !> tag; it should be "scalar", not "'+e.kind+'"'),s=0,u=e.implicitTypes.length;s"),null!==e.result&&f.kind!==e.kind&&ce(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+f.kind+'", not "'+e.kind+'"'),f.resolve(e.result,e.tag)?(e.result=f.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):ce(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||y}function ke(e){var t,n,i,r,o=e.position,a=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(r=e.input.charCodeAt(e.position))&&(ge(e,!0,-1),r=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==r));){for(a=!0,r=e.input.charCodeAt(++e.position),t=e.position;0!==r&&!z(r);)r=e.input.charCodeAt(++e.position);for(i=[],(n=e.input.slice(t,e.position)).length<1&&ce(e,"directive name must not be less than one character in length");0!==r;){for(;Q(r);)r=e.input.charCodeAt(++e.position);if(35===r){do{r=e.input.charCodeAt(++e.position)}while(0!==r&&!J(r));break}if(J(r))break;for(t=e.position;0!==r&&!z(r);)r=e.input.charCodeAt(++e.position);i.push(e.input.slice(t,e.position))}0!==r&&he(e),P.call(ue,n)?ue[n](e,n,i):se(e,'unknown document directive "'+n+'"')}ge(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,ge(e,!0,-1)):a&&ce(e,"directives end mark is expected"),we(e,e.lineIndent-1,4,!1,!0),ge(e,!0,-1),e.checkLineBreaks&&H.test(e.input.slice(o,e.position))&&se(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&me(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,ge(e,!0,-1)):e.position=q){if(s=W.limit_backward,W.limit_backward=q,W.ket=W.cursor,e=W.find_among_b(P,7))switch(W.bra=W.cursor,e){case 1:if(l()){if(i=W.limit-W.cursor,!W.eq_s_b(1,"s")&&(W.cursor=W.limit-i,!W.eq_s_b(1,"t")))break;W.slice_del()}break;case 2:W.slice_from("i");break;case 3:W.slice_del();break;case 4:W.eq_s_b(2,"gu")&&W.slice_del()}W.limit_backward=s}}function b(){var e=W.limit-W.cursor;W.find_among_b(U,5)&&(W.cursor=W.limit-e,W.ket=W.cursor,W.cursor>W.limit_backward&&(W.cursor--,W.bra=W.cursor,W.slice_del()))}function d(){for(var e,r=1;W.out_grouping_b(F,97,251);)r--;if(r<=0){if(W.ket=W.cursor,e=W.limit-W.cursor,!W.eq_s_b(1,"é")&&(W.cursor=W.limit-e,!W.eq_s_b(1,"è")))return;W.bra=W.cursor,W.slice_from("e")}}function k(){if(!w()&&(W.cursor=W.limit,!f()&&(W.cursor=W.limit,!m())))return W.cursor=W.limit,void _();W.cursor=W.limit,W.ket=W.cursor,W.eq_s_b(1,"Y")?(W.bra=W.cursor,W.slice_from("i")):(W.cursor=W.limit,W.eq_s_b(1,"ç")&&(W.bra=W.cursor,W.slice_from("c")))}var p,g,q,v=[new r("col",-1,-1),new r("par",-1,-1),new r("tap",-1,-1)],h=[new r("",-1,4),new r("I",0,1),new r("U",0,2),new r("Y",0,3)],z=[new r("iqU",-1,3),new r("abl",-1,3),new r("Ièr",-1,4),new r("ièr",-1,4),new r("eus",-1,2),new r("iv",-1,1)],y=[new r("ic",-1,2),new r("abil",-1,1),new r("iv",-1,3)],C=[new r("iqUe",-1,1),new r("atrice",-1,2),new r("ance",-1,1),new r("ence",-1,5),new r("logie",-1,3),new r("able",-1,1),new r("isme",-1,1),new r("euse",-1,11),new r("iste",-1,1),new r("ive",-1,8),new r("if",-1,8),new r("usion",-1,4),new r("ation",-1,2),new r("ution",-1,4),new r("ateur",-1,2),new r("iqUes",-1,1),new r("atrices",-1,2),new r("ances",-1,1),new r("ences",-1,5),new r("logies",-1,3),new r("ables",-1,1),new r("ismes",-1,1),new r("euses",-1,11),new r("istes",-1,1),new r("ives",-1,8),new r("ifs",-1,8),new r("usions",-1,4),new r("ations",-1,2),new r("utions",-1,4),new r("ateurs",-1,2),new r("ments",-1,15),new r("ements",30,6),new r("issements",31,12),new r("ités",-1,7),new r("ment",-1,15),new r("ement",34,6),new r("issement",35,12),new r("amment",34,13),new r("emment",34,14),new r("aux",-1,10),new r("eaux",39,9),new r("eux",-1,1),new r("ité",-1,7)],x=[new r("ira",-1,1),new r("ie",-1,1),new r("isse",-1,1),new r("issante",-1,1),new r("i",-1,1),new r("irai",4,1),new r("ir",-1,1),new r("iras",-1,1),new r("ies",-1,1),new r("îmes",-1,1),new r("isses",-1,1),new r("issantes",-1,1),new r("îtes",-1,1),new r("is",-1,1),new r("irais",13,1),new r("issais",13,1),new r("irions",-1,1),new r("issions",-1,1),new r("irons",-1,1),new r("issons",-1,1),new r("issants",-1,1),new r("it",-1,1),new r("irait",21,1),new r("issait",21,1),new r("issant",-1,1),new r("iraIent",-1,1),new r("issaIent",-1,1),new r("irent",-1,1),new r("issent",-1,1),new r("iront",-1,1),new r("ît",-1,1),new r("iriez",-1,1),new r("issiez",-1,1),new r("irez",-1,1),new r("issez",-1,1)],I=[new r("a",-1,3),new r("era",0,2),new r("asse",-1,3),new r("ante",-1,3),new r("ée",-1,2),new r("ai",-1,3),new r("erai",5,2),new r("er",-1,2),new r("as",-1,3),new r("eras",8,2),new r("âmes",-1,3),new r("asses",-1,3),new r("antes",-1,3),new r("âtes",-1,3),new r("ées",-1,2),new r("ais",-1,3),new r("erais",15,2),new r("ions",-1,1),new r("erions",17,2),new r("assions",17,3),new r("erons",-1,2),new r("ants",-1,3),new r("és",-1,2),new r("ait",-1,3),new r("erait",23,2),new r("ant",-1,3),new r("aIent",-1,3),new r("eraIent",26,2),new r("èrent",-1,2),new r("assent",-1,3),new r("eront",-1,2),new r("ât",-1,3),new r("ez",-1,2),new r("iez",32,2),new r("eriez",33,2),new r("assiez",33,3),new r("erez",32,2),new r("é",-1,2)],P=[new r("e",-1,3),new r("Ière",0,2),new r("ière",0,2),new r("ion",-1,1),new r("Ier",-1,2),new r("ier",-1,2),new r("ë",-1,4)],U=[new r("ell",-1,-1),new r("eill",-1,-1),new r("enn",-1,-1),new r("onn",-1,-1),new r("ett",-1,-1)],F=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,128,130,103,8,5],S=[1,65,20,0,0,0,0,0,0,0,0,0,0,0,0,0,128],W=new s;this.setCurrent=function(e){W.setCurrent(e)},this.getCurrent=function(){return W.getCurrent()},this.stem=function(){var e=W.cursor;return n(),W.cursor=e,u(),W.limit_backward=e,W.cursor=W.limit,k(),W.cursor=W.limit,b(),W.cursor=W.limit,d(),W.cursor=W.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.fr.stemmer,"stemmer-fr"),e.fr.stopWordFilter=e.generateStopWordFilter("ai aie aient aies ait as au aura aurai auraient aurais aurait auras aurez auriez aurions aurons auront aux avaient avais avait avec avez aviez avions avons ayant ayez ayons c ce ceci celà ces cet cette d dans de des du elle en es est et eu eue eues eurent eus eusse eussent eusses eussiez eussions eut eux eûmes eût eûtes furent fus fusse fussent fusses fussiez fussions fut fûmes fût fûtes ici il ils j je l la le les leur leurs lui m ma mais me mes moi mon même n ne nos notre nous on ont ou par pas pour qu que quel quelle quelles quels qui s sa sans se sera serai seraient serais serait seras serez seriez serions serons seront ses soi soient sois soit sommes son sont soyez soyons suis sur t ta te tes toi ton tu un une vos votre vous y à étaient étais était étant étiez étions été étée étées étés êtes".split(" ")),e.Pipeline.registerFunction(e.fr.stopWordFilter,"stopWordFilter-fr")}});
\ No newline at end of file
diff --git a/cloudbank/js/lunr/lunr.hi.min.js b/cloudbank/js/lunr/lunr.hi.min.js
new file mode 100644
index 000000000..7dbc41402
--- /dev/null
+++ b/cloudbank/js/lunr/lunr.hi.min.js
@@ -0,0 +1 @@
+!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hi=function(){this.pipeline.reset(),this.pipeline.add(e.hi.trimmer,e.hi.stopWordFilter,e.hi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hi.stemmer))},e.hi.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿa-zA-Za-zA-Z0-90-9",e.hi.trimmer=e.trimmerSupport.generateTrimmer(e.hi.wordCharacters),e.Pipeline.registerFunction(e.hi.trimmer,"trimmer-hi"),e.hi.stopWordFilter=e.generateStopWordFilter("अत अपना अपनी अपने अभी अंदर आदि आप इत्यादि इन इनका इन्हीं इन्हें इन्हों इस इसका इसकी इसके इसमें इसी इसे उन उनका उनकी उनके उनको उन्हीं उन्हें उन्हों उस उसके उसी उसे एक एवं एस ऐसे और कई कर करता करते करना करने करें कहते कहा का काफ़ी कि कितना किन्हें किन्हों किया किर किस किसी किसे की कुछ कुल के को कोई कौन कौनसा गया घर जब जहाँ जा जितना जिन जिन्हें जिन्हों जिस जिसे जीधर जैसा जैसे जो तक तब तरह तिन तिन्हें तिन्हों तिस तिसे तो था थी थे दबारा दिया दुसरा दूसरे दो द्वारा न नके नहीं ना निहायत नीचे ने पर पहले पूरा पे फिर बनी बही बहुत बाद बाला बिलकुल भी भीतर मगर मानो मे में यदि यह यहाँ यही या यिह ये रखें रहा रहे ऱ्वासा लिए लिये लेकिन व वग़ैरह वर्ग वह वहाँ वहीं वाले वुह वे वो सकता सकते सबसे सभी साथ साबुत साभ सारा से सो संग ही हुआ हुई हुए है हैं हो होता होती होते होना होने".split(" ")),e.hi.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.hi.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var t=i.toString().toLowerCase().replace(/^\s+/,"");return r.cut(t).split("|")},e.Pipeline.registerFunction(e.hi.stemmer,"stemmer-hi"),e.Pipeline.registerFunction(e.hi.stopWordFilter,"stopWordFilter-hi")}});
\ No newline at end of file
diff --git a/cloudbank/js/lunr/lunr.hu.min.js b/cloudbank/js/lunr/lunr.hu.min.js
new file mode 100644
index 000000000..ed9d909f7
--- /dev/null
+++ b/cloudbank/js/lunr/lunr.hu.min.js
@@ -0,0 +1,18 @@
+/*!
+ * Lunr languages, `Hungarian` language
+ * https://github.com/MihaiValentin/lunr-languages
+ *
+ * Copyright 2014, Mihai Valentin
+ * http://www.mozilla.org/MPL/
+ */
+/*!
+ * based on
+ * Snowball JavaScript Library v0.3
+ * http://code.google.com/p/urim/
+ * http://snowball.tartarus.org/
+ *
+ * Copyright 2010, Oleg Mazko
+ * http://www.mozilla.org/MPL/
+ */
+
+!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hu=function(){this.pipeline.reset(),this.pipeline.add(e.hu.trimmer,e.hu.stopWordFilter,e.hu.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hu.stemmer))},e.hu.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.hu.trimmer=e.trimmerSupport.generateTrimmer(e.hu.wordCharacters),e.Pipeline.registerFunction(e.hu.trimmer,"trimmer-hu"),e.hu.stemmer=function(){var n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,n=L.cursor;if(d=L.limit,L.in_grouping(W,97,252))for(;;){if(e=L.cursor,L.out_grouping(W,97,252))return L.cursor=e,L.find_among(g,8)||(L.cursor=e,e
=e;t--){var r=this.uncheckedNodes[t],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r["char"]]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}},e.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},e.Index.prototype.search=function(t){return this.query(function(r){var i=new e.QueryParser(t,r);i.parse()})},e.Index.prototype.query=function(t){for(var r=new e.Query(this.fields),i=Object.create(null),n=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}});
\ No newline at end of file
diff --git a/cloudbank/js/lunr/lunr.pt.min.js b/cloudbank/js/lunr/lunr.pt.min.js
new file mode 100644
index 000000000..6c16996d6
--- /dev/null
+++ b/cloudbank/js/lunr/lunr.pt.min.js
@@ -0,0 +1,18 @@
+/*!
+ * Lunr languages, `Portuguese` language
+ * https://github.com/MihaiValentin/lunr-languages
+ *
+ * Copyright 2014, Mihai Valentin
+ * http://www.mozilla.org/MPL/
+ */
+/*!
+ * based on
+ * Snowball JavaScript Library v0.3
+ * http://code.google.com/p/urim/
+ * http://snowball.tartarus.org/
+ *
+ * Copyright 2010, Oleg Mazko
+ * http://www.mozilla.org/MPL/
+ */
+
+!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.pt=function(){this.pipeline.reset(),this.pipeline.add(e.pt.trimmer,e.pt.stopWordFilter,e.pt.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.pt.stemmer))},e.pt.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.pt.trimmer=e.trimmerSupport.generateTrimmer(e.pt.wordCharacters),e.Pipeline.registerFunction(e.pt.trimmer,"trimmer-pt"),e.pt.stemmer=function(){var r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,n=new function(){function e(){for(var e;;){if(z.bra=z.cursor,e=z.find_among(k,3))switch(z.ket=z.cursor,e){case 1:z.slice_from("a~");continue;case 2:z.slice_from("o~");continue;case 3:if(z.cursor>=z.limit)break;z.cursor++;continue}break}}function n(){if(z.out_grouping(y,97,250)){for(;!z.in_grouping(y,97,250);){if(z.cursor>=z.limit)return!0;z.cursor++}return!1}return!0}function i(){if(z.in_grouping(y,97,250))for(;!z.out_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}return g=z.cursor,!0}function o(){var e,r,s=z.cursor;if(z.in_grouping(y,97,250))if(e=z.cursor,n()){if(z.cursor=e,i())return}else g=z.cursor;if(z.cursor=s,z.out_grouping(y,97,250)){if(r=z.cursor,n()){if(z.cursor=r,!z.in_grouping(y,97,250)||z.cursor>=z.limit)return;z.cursor++}g=z.cursor}}function t(){for(;!z.in_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}for(;!z.out_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}return!0}function a(){var e=z.cursor;g=z.limit,b=g,h=g,o(),z.cursor=e,t()&&(b=z.cursor,t()&&(h=z.cursor))}function u(){for(var e;;){if(z.bra=z.cursor,e=z.find_among(q,3))switch(z.ket=z.cursor,e){case 1:z.slice_from("ã");continue;case 2:z.slice_from("õ");continue;case 3:if(z.cursor>=z.limit)break;z.cursor++;continue}break}}function w(){return g<=z.cursor}function m(){return b<=z.cursor}function c(){return h<=z.cursor}function l(){var e;if(z.ket=z.cursor,!(e=z.find_among_b(F,45)))return!1;switch(z.bra=z.cursor,e){case 1:if(!c())return!1;z.slice_del();break;case 2:if(!c())return!1;z.slice_from("log");break;case 3:if(!c())return!1;z.slice_from("u");break;case 4:if(!c())return!1;z.slice_from("ente");break;case 5:if(!m())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(j,4),e&&(z.bra=z.cursor,c()&&(z.slice_del(),1==e&&(z.ket=z.cursor,z.eq_s_b(2,"at")&&(z.bra=z.cursor,c()&&z.slice_del()))));break;case 6:if(!c())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(C,3),e&&(z.bra=z.cursor,1==e&&c()&&z.slice_del());break;case 7:if(!c())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(P,3),e&&(z.bra=z.cursor,1==e&&c()&&z.slice_del());break;case 8:if(!c())return!1;z.slice_del(),z.ket=z.cursor,z.eq_s_b(2,"at")&&(z.bra=z.cursor,c()&&z.slice_del());break;case 9:if(!w()||!z.eq_s_b(1,"e"))return!1;z.slice_from("ir")}return!0}function f(){var e,r;if(z.cursor>=g){if(r=z.limit_backward,z.limit_backward=g,z.ket=z.cursor,e=z.find_among_b(S,120))return z.bra=z.cursor,1==e&&z.slice_del(),z.limit_backward=r,!0;z.limit_backward=r}return!1}function d(){var e;z.ket=z.cursor,(e=z.find_among_b(W,7))&&(z.bra=z.cursor,1==e&&w()&&z.slice_del())}function v(e,r){if(z.eq_s_b(1,e)){z.bra=z.cursor;var s=z.limit-z.cursor;if(z.eq_s_b(1,r))return z.cursor=z.limit-s,w()&&z.slice_del(),!1}return!0}function p(){var e;if(z.ket=z.cursor,e=z.find_among_b(L,4))switch(z.bra=z.cursor,e){case 1:w()&&(z.slice_del(),z.ket=z.cursor,z.limit-z.cursor,v("u","g")&&v("i","c"));break;case 2:z.slice_from("c")}}function _(){if(!l()&&(z.cursor=z.limit,!f()))return z.cursor=z.limit,void d();z.cursor=z.limit,z.ket=z.cursor,z.eq_s_b(1,"i")&&(z.bra=z.cursor,z.eq_s_b(1,"c")&&(z.cursor=z.limit,w()&&z.slice_del()))}var h,b,g,k=[new r("",-1,3),new r("ã",0,1),new r("õ",0,2)],q=[new r("",-1,3),new r("a~",0,1),new r("o~",0,2)],j=[new r("ic",-1,-1),new r("ad",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],C=[new r("ante",-1,1),new r("avel",-1,1),new r("ível",-1,1)],P=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],F=[new r("ica",-1,1),new r("ância",-1,1),new r("ência",-1,4),new r("ira",-1,9),new r("adora",-1,1),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,8),new r("eza",-1,1),new r("logía",-1,2),new r("idade",-1,7),new r("ante",-1,1),new r("mente",-1,6),new r("amente",12,5),new r("ável",-1,1),new r("ível",-1,1),new r("ución",-1,3),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,1),new r("imento",-1,1),new r("ivo",-1,8),new r("aça~o",-1,1),new r("ador",-1,1),new r("icas",-1,1),new r("ências",-1,4),new r("iras",-1,9),new r("adoras",-1,1),new r("osas",-1,1),new r("istas",-1,1),new r("ivas",-1,8),new r("ezas",-1,1),new r("logías",-1,2),new r("idades",-1,7),new r("uciones",-1,3),new r("adores",-1,1),new r("antes",-1,1),new r("aço~es",-1,1),new r("icos",-1,1),new r("ismos",-1,1),new r("osos",-1,1),new r("amentos",-1,1),new r("imentos",-1,1),new r("ivos",-1,8)],S=[new r("ada",-1,1),new r("ida",-1,1),new r("ia",-1,1),new r("aria",2,1),new r("eria",2,1),new r("iria",2,1),new r("ara",-1,1),new r("era",-1,1),new r("ira",-1,1),new r("ava",-1,1),new r("asse",-1,1),new r("esse",-1,1),new r("isse",-1,1),new r("aste",-1,1),new r("este",-1,1),new r("iste",-1,1),new r("ei",-1,1),new r("arei",16,1),new r("erei",16,1),new r("irei",16,1),new r("am",-1,1),new r("iam",20,1),new r("ariam",21,1),new r("eriam",21,1),new r("iriam",21,1),new r("aram",20,1),new r("eram",20,1),new r("iram",20,1),new r("avam",20,1),new r("em",-1,1),new r("arem",29,1),new r("erem",29,1),new r("irem",29,1),new r("assem",29,1),new r("essem",29,1),new r("issem",29,1),new r("ado",-1,1),new r("ido",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("indo",-1,1),new r("ara~o",-1,1),new r("era~o",-1,1),new r("ira~o",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("ir",-1,1),new r("as",-1,1),new r("adas",47,1),new r("idas",47,1),new r("ias",47,1),new r("arias",50,1),new r("erias",50,1),new r("irias",50,1),new r("aras",47,1),new r("eras",47,1),new r("iras",47,1),new r("avas",47,1),new r("es",-1,1),new r("ardes",58,1),new r("erdes",58,1),new r("irdes",58,1),new r("ares",58,1),new r("eres",58,1),new r("ires",58,1),new r("asses",58,1),new r("esses",58,1),new r("isses",58,1),new r("astes",58,1),new r("estes",58,1),new r("istes",58,1),new r("is",-1,1),new r("ais",71,1),new r("eis",71,1),new r("areis",73,1),new r("ereis",73,1),new r("ireis",73,1),new r("áreis",73,1),new r("éreis",73,1),new r("íreis",73,1),new r("ásseis",73,1),new r("ésseis",73,1),new r("ísseis",73,1),new r("áveis",73,1),new r("íeis",73,1),new r("aríeis",84,1),new r("eríeis",84,1),new r("iríeis",84,1),new r("ados",-1,1),new r("idos",-1,1),new r("amos",-1,1),new r("áramos",90,1),new r("éramos",90,1),new r("íramos",90,1),new r("ávamos",90,1),new r("íamos",90,1),new r("aríamos",95,1),new r("eríamos",95,1),new r("iríamos",95,1),new r("emos",-1,1),new r("aremos",99,1),new r("eremos",99,1),new r("iremos",99,1),new r("ássemos",99,1),new r("êssemos",99,1),new r("íssemos",99,1),new r("imos",-1,1),new r("armos",-1,1),new r("ermos",-1,1),new r("irmos",-1,1),new r("ámos",-1,1),new r("arás",-1,1),new r("erás",-1,1),new r("irás",-1,1),new r("eu",-1,1),new r("iu",-1,1),new r("ou",-1,1),new r("ará",-1,1),new r("erá",-1,1),new r("irá",-1,1)],W=[new r("a",-1,1),new r("i",-1,1),new r("o",-1,1),new r("os",-1,1),new r("á",-1,1),new r("í",-1,1),new r("ó",-1,1)],L=[new r("e",-1,1),new r("ç",-1,2),new r("é",-1,1),new r("ê",-1,1)],y=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,3,19,12,2],z=new s;this.setCurrent=function(e){z.setCurrent(e)},this.getCurrent=function(){return z.getCurrent()},this.stem=function(){var r=z.cursor;return e(),z.cursor=r,a(),z.limit_backward=r,z.cursor=z.limit,_(),z.cursor=z.limit,p(),z.cursor=z.limit_backward,u(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.pt.stemmer,"stemmer-pt"),e.pt.stopWordFilter=e.generateStopWordFilter("a ao aos aquela aquelas aquele aqueles aquilo as até com como da das de dela delas dele deles depois do dos e ela elas ele eles em entre era eram essa essas esse esses esta estamos estas estava estavam este esteja estejam estejamos estes esteve estive estivemos estiver estivera estiveram estiverem estivermos estivesse estivessem estivéramos estivéssemos estou está estávamos estão eu foi fomos for fora foram forem formos fosse fossem fui fôramos fôssemos haja hajam hajamos havemos hei houve houvemos houver houvera houveram houverei houverem houveremos houveria houveriam houvermos houverá houverão houveríamos houvesse houvessem houvéramos houvéssemos há hão isso isto já lhe lhes mais mas me mesmo meu meus minha minhas muito na nas nem no nos nossa nossas nosso nossos num numa não nós o os ou para pela pelas pelo pelos por qual quando que quem se seja sejam sejamos sem serei seremos seria seriam será serão seríamos seu seus somos sou sua suas são só também te tem temos tenha tenham tenhamos tenho terei teremos teria teriam terá terão teríamos teu teus teve tinha tinham tive tivemos tiver tivera tiveram tiverem tivermos tivesse tivessem tivéramos tivéssemos tu tua tuas tém tínhamos um uma você vocês vos à às éramos".split(" ")),e.Pipeline.registerFunction(e.pt.stopWordFilter,"stopWordFilter-pt")}});
\ No newline at end of file
diff --git a/cloudbank/js/lunr/lunr.ro.min.js b/cloudbank/js/lunr/lunr.ro.min.js
new file mode 100644
index 000000000..727714018
--- /dev/null
+++ b/cloudbank/js/lunr/lunr.ro.min.js
@@ -0,0 +1,18 @@
+/*!
+ * Lunr languages, `Romanian` language
+ * https://github.com/MihaiValentin/lunr-languages
+ *
+ * Copyright 2014, Mihai Valentin
+ * http://www.mozilla.org/MPL/
+ */
+/*!
+ * based on
+ * Snowball JavaScript Library v0.3
+ * http://code.google.com/p/urim/
+ * http://snowball.tartarus.org/
+ *
+ * Copyright 2010, Oleg Mazko
+ * http://www.mozilla.org/MPL/
+ */
+
+!function(e,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ro=function(){this.pipeline.reset(),this.pipeline.add(e.ro.trimmer,e.ro.stopWordFilter,e.ro.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ro.stemmer))},e.ro.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.ro.trimmer=e.trimmerSupport.generateTrimmer(e.ro.wordCharacters),e.Pipeline.registerFunction(e.ro.trimmer,"trimmer-ro"),e.ro.stemmer=function(){var i=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){function e(e,i){L.eq_s(1,e)&&(L.ket=L.cursor,L.in_grouping(W,97,259)&&L.slice_from(i))}function n(){for(var i,r;;){if(i=L.cursor,L.in_grouping(W,97,259)&&(r=L.cursor,L.bra=r,e("u","U"),L.cursor=r,e("i","I")),L.cursor=i,L.cursor>=L.limit)break;L.cursor++}}function t(){if(L.out_grouping(W,97,259)){for(;!L.in_grouping(W,97,259);){if(L.cursor>=L.limit)return!0;L.cursor++}return!1}return!0}function a(){if(L.in_grouping(W,97,259))for(;!L.out_grouping(W,97,259);){if(L.cursor>=L.limit)return!0;L.cursor++}return!1}function o(){var e,i,r=L.cursor;if(L.in_grouping(W,97,259)){if(e=L.cursor,!t())return void(h=L.cursor);if(L.cursor=e,!a())return void(h=L.cursor)}L.cursor=r,L.out_grouping(W,97,259)&&(i=L.cursor,t()&&(L.cursor=i,L.in_grouping(W,97,259)&&L.cursor0&&t<20&&n>0&&n<11}function _(e){return i.default.getInstance().style===e}function C(e){if(!e.hasAttribute("annotation"))return!1;const t=e.getAttribute("annotation");return!!/clearspeak:simple$|clearspeak:simple;/.exec(t)}function T(e){if(C(e))return!0;if("subscript"!==e.tagName)return!1;const t=e.childNodes[0].childNodes,n=t[1];return"identifier"===t[0].tagName&&(v(n)||"infixop"===n.tagName&&n.hasAttribute("role")&&"implicit"===n.getAttribute("role")&&O(n))}function v(e){return"number"===e.tagName&&e.hasAttribute("role")&&"integer"===e.getAttribute("role")}function O(e){return o.evalXPath("children/*",e).every((e=>v(e)||"identifier"===e.tagName))}function M(e){return"text"===e.type||"punctuated"===e.type&&"text"===e.role&&E(e.childNodes[0])&&I(e.childNodes.slice(1))||"identifier"===e.type&&"unit"===e.role||"infixop"===e.type&&("implicit"===e.role||"unit"===e.role)}function I(e){for(let t=0;t