• Overview
    Frameworks
    • Django
    • Next.js
    • Drupal
    • WordPress
    • Symfony
    • Magento
    • See all frameworks
    Features
    • Observability
    • Auto-scaling
    • Multi-framework
    • Security
    Solutions
    • Marketing Teams
    • Retail
    • Higher Education
  • Pricing
  • Featured articles
    • Switching to Platform.sh can help IT/DevOps organizations drive 219% ROI
    • Organizations, the ultimate way to manage your users and projects
  • Support
  • Docs
  • Contact
  • Login
  • Free Trial
Blog
Microservices in the cloud, part one

Microservices in the cloud, part two

java
August 23, 2019
Otavio Santana
Otavio Santana
Developer Relations

Microservices are a hot topic in software design, and for good reason. They have plenty of advantages when it comes to handling infrastructure complexity, many of which were addressed in part one of our related Java post. Now, it’s time to talk about the code and design. In this post, we’ll take a deep dive into each module.

When creating an application, clean code means thinking about design and architecture. Architecture is the software process that handles flexibility, scalability, usability, security, and other points, so you have more time to focus on business rather than on technology. Some architecture examples include:

  • Serverless architecture. Application designs that incorporate third-party, Backend-as-a-Service (BaaS) services, and include custom code run in managed, ephemeral containers on a Functions-as-a-Service (FaaS) platform.
  • Event-driven architecture. A software architecture pattern promoting the production, detection, consumption of, and reaction to events.
  • Microservices architecture. A variant of the service-oriented architecture (SOA) style that structures an application as a collection of loosely coupled services. In a microservices architecture, services are fine grained and the protocols lightweight.

The design has a low-level duty that supervises the code, such as what each module is going to do, the class's scope, the functions proposal, and so on.

  • SOLID. The five design principles that make software designs more understandable, flexible, and maintainable.
  • Design patterns. The ideal solutions to common problems in software design. Each pattern is like a blueprint you can customize to solve a particular design problem in your code.

Speaker service

The first service that we'll cover is the Speaker service, which uses Thorntail and PostgreSQL. The Eclipse MicroProfile doesn’t have support for a relational database. However, we can use the specification from its older and more mature brother, Jakarta EE. As a first step, let's define the Speaker entity.

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;

@Entity
@Table(name = "speaker")
public class Speaker {

    @Id
    @GeneratedValue
    private Integer id;

    @Column
    private String name;

    @Column
    private String bio;

    @Column
    private String twitter;

    @Column
    private String github;

    //more code

}

Revisit our previous article that covers the JPA integration, using Transactional annotation and a CDI Interceptor to manage the transaction.

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;

@ApplicationScoped
public class SpeakerService {

    @Inject
    private EntityManager entityManager;

    @Transactional
    public Speaker insert(Speaker speaker) {
        entityManager.persist(speaker);
        return speaker;
    }

    @Transactional
    public void update(Speaker speaker) {
        entityManager.persist(speaker);
    }

    @Transactional
    public void delete(Integer id) {
        find(id).ifPresent(c -> entityManager.remove(c));
    }


    public Optional<Speaker> find(Integer id) {
        return Optional.ofNullable(entityManager.find(Speaker.class, id));
    }

    public List<Speaker> findAll() {
        String query = "select e from Speaker e";
        return entityManager.createQuery(query).getResultList();
    }
}

We have a resource to expose the services through an HTTP request. We've created a layer on DTO to avoid losing the encapsulation.

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;

@Path("speakers")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public class SpeakerResource {

    @Inject
    private SpeakerService speakerService;

    @GET
    public List<SpeakerDTO> findAll() {
        return speakerService.findAll()
                .stream().map(SpeakerDTO::of)
                .collect(Collectors.toList());
    }

    @GET
    @Path("{id}")
    public SpeakerDTO findById(@PathParam("id") Integer id) {
        final Optional<Speaker> conference = speakerService.find(id);
        return conference.map(SpeakerDTO::of).orElseThrow(this::notFound);
    }

    @PUT
    @Path("{id}")
    public SpeakerDTO update(@PathParam("id") Integer id, @Valid SpeakerDTO speakerUpdated) {
        final Optional<Speaker> optional = speakerService.find(id);
        final Speaker speaker = optional.orElseThrow(() -> notFound());
        speaker.update(Speaker.of(speakerUpdated));
        speakerService.update(speaker);
        return SpeakerDTO.of(speaker);
    }

    @DELETE
    @Path("{id}")
    public Response remove(@PathParam("id") Integer id) {
        speakerService.delete(id);
        return status(NO_CONTENT).build();
    }

    @POST
    public SpeakerDTO insert(@Valid SpeakerDTO speaker) {
        return SpeakerDTO.of(speakerService.insert(Speaker.of(speaker)));
    }

    private WebApplicationException notFound() {
        return new WebApplicationException(Response.Status.NOT_FOUND);
    }
}

In the .platform.app.yaml file, we need to check that the speaker application has access to the PostgreSQL instance and anything else about the database that lets Platform.sh handle it.

relationships:
  postgresql: "postgresql:postgresql"

Session service

The Session service will handle the contents of the conference, so that a user can find sessions related to topics like cloud and Java. This service uses Elasticsearch with Jakarta NoSQL and KumuluzEE.

import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;

import java.util.Objects;

@Entity
public class Session {

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String title;

    @Column
    private String description;

    @Column
    private String conference;

    @Column
    private Integer speaker;

}

We're applying a distinct aspect of Elasticsearch to use it as a search engine. Jakarta NoSQL looks to the specialization that allows particular features for each NoSQL database. Therefore, we'll have a mix of repository and ElasticsearchTemplate, a specialty of DocumentTemplate.

import jakarta.nosql.mapping.Repository;

import java.util.List;

public interface SessionRepository extends Repository<Session, String> {

    List<Session> findAll();
}


import org.elasticsearch.index.query.QueryBuilder;
import org.jnosql.artemis.elasticsearch.document.ElasticsearchTemplate;
import org.jnosql.artemis.util.StringUtils;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;

@Path("sessions")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON + "; charset=UTF-8")
public class SessionResource {

    private static Logger LOGGER = Logger.getLogger(SessionResource.class.getName());

    @Inject
    private SessionRepository speakerRepository;

    @Inject
    private ElasticsearchTemplate template;

    @GET
    public List<SessionDTO> findAll(@QueryParam("search") String search) {
        LOGGER.info("searching with the field: " + search);
        if (StringUtils.isNotBlank(search)) {
            QueryBuilder queryBuilder = boolQuery()
                    .should(termQuery("name", search))
                    .should(termQuery("title", search))
                    .should(termQuery("description", search));

            LOGGER.info("the query: " + queryBuilder);
            List<Session> sessions = template.search(queryBuilder, "Session");
            LOGGER.info("the result: " + sessions);
            return sessions.stream()
                    .map(SessionDTO::of)
                    .collect(Collectors.toList());
        }
        return speakerRepository.findAll().stream()
                .map(SessionDTO::of).collect(Collectors.toList());
    }

    @GET
    @Path("{id}")
    public Session findById(@PathParam("id") String id) {
        final Optional<Session> conference = speakerRepository.findById(id);
        return conference.orElseThrow(this::notFound);
    }

    @PUT
    @Path("{id}")
    public SessionDTO update(@PathParam("id") String id, @Valid SessionDTO sessionUpdated) {
        final Optional<Session> optional = speakerRepository.findById(id);
        final Session session = optional.orElseThrow(() -> notFound());
        session.update(Session.of(sessionUpdated));
        speakerRepository.save(session);
        return SessionDTO.of(session);
    }

    @DELETE
    @Path("{id}")
    public Response remove(@PathParam("id") String id) {
        speakerRepository.deleteById(id);
        return status(NO_CONTENT).build();
    }

    @POST
    public SessionDTO insert(@Valid SessionDTO session) {
        session.setId(UUID.randomUUID().toString());
        return SessionDTO.of(speakerRepository.save(Session.of(session)));
    }

    private WebApplicationException notFound() {
        return new WebApplicationException(Response.Status.NOT_FOUND);
    }
}

Conference service

The Conference service is the most connected of the services because it needs to keep information from both speaker and session services. It uses Payara Micro and MongoDB with Jakarta NoSQL.

import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;

import java.time.Year;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

@Entity
public class Conference {

    @Id
    @Convert(ObjectIdConverter.class)
    private String id;

    @Column
    private String name;

    @Column
    private String city;

    @Column
    private String link;

    @Column
    @Convert(YearConverter.class)
    private Year year;

    @Column
    private List<Speaker> speakers;

    @Column
    private List<Session> sessions;

}

@Entity
public class Session {

    @Column
    private String id;

    @Column
    private String name;
}

@Entity
public class Speaker {

    @Column
    private Integer id;

    @Column
    private String name;

}

One of the benefits of MongoDB is that we can use the subdocument instead of creating a relationship. Therefore, we can embed Speaker and Session instead of doing joins. Note, the Speaker and Session entities have brief information such as name and ID.

import jakarta.nosql.mapping.Repository;

import java.util.List;

public interface ConferenceRepository extends Repository<Conference, String> {

    List<Conference> findAll();
}


import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;

@Path("conferences")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public class ConferenceResource {

    @Inject
    private ConferenceRepository conferenceRepository;

    @GET
    public List<ConferenceDTO> findAll() {
        return conferenceRepository.findAll().stream()
                .map(ConferenceDTO::of)
                .collect(Collectors.toList());
    }

    @GET
    @Path("{id}")
    public ConferenceDTO findById(@PathParam("id") String id) {
        final Optional<Conference> conference = conferenceRepository.findById(id);
        return conference.map(ConferenceDTO::of).orElseThrow(this::notFound);
    }

    @PUT
    @Path("{id}")
    public ConferenceDTO update(@PathParam("id") String id, @Valid ConferenceDTO conferenceUpdated) {
        final Optional<Conference> optional = conferenceRepository.findById(id);
        final Conference conference = optional.orElseThrow(() -> notFound());
        conference.update(Conference.of(conferenceUpdated));
        conferenceRepository.save(conference);
        return ConferenceDTO.of(conference);
    }

    @DELETE
    @Path("{id}")
    public Response remove(@PathParam("id") String id) {
        conferenceRepository.deleteById(id);
        return status(NO_CONTENT).build();
    }

    @POST
    public ConferenceDTO insert(@Valid ConferenceDTO conference) {
        return ConferenceDTO.of(conferenceRepository.save(Conference.of(conference)));
    }

    private WebApplicationException notFound() {
        return new WebApplicationException(Response.Status.NOT_FOUND);
    }
}

Client service

The client will show the RESTful application using HTML 5 with Eclipse Krazo. Yes, Eclipse Krazo has several engine extensions to use more than JSP, such as HTML 5. The extension we'll use is Thymeleaf with Apache TomEE.

    <dependencies>
        <dependency>
            <groupId>org.eclipse.microprofile</groupId>
            <artifactId>microprofile</artifactId>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>sh.platform</groupId>
            <artifactId>config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.eclipse.krazo</groupId>
            <artifactId>krazo-core</artifactId>
            <version>${version.krazo}</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.krazo</groupId>
            <artifactId>krazo-cxf</artifactId>
            <version>${version.krazo}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>${jstl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.krazo.ext</groupId>
            <artifactId>krazo-thymeleaf</artifactId>
            <version>${version.krazo}</version>
        </dependency>
    </dependencies>

The first step in the client is to create the bridge to request information from the services. Thankfully, we have an Eclipse MicroProfile Rest Client to deal just with interfaces and nothing more.

package org.jespanol.client.speaker;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("speakers")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface SpeakerService {

    @GET
    List<Speaker> findAll();

    @GET
    @Path("{id}")
    Speaker findById(@PathParam("id") Integer id);

    @PUT
    @Path("{id}")
    Speaker update(@PathParam("id") Integer id, Speaker speaker);

    @DELETE
    @Path("{id}")
    Response remove(@PathParam("id") Integer id);

    @POST
    Speaker insert(Speaker speaker);

}


import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("sessions")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface SessionService {

    @GET
    List<Session> findAll(@QueryParam("search") String search);

    @GET
    List<Session> findAll();

    @GET
    @Path("{id}")
    Session findById(@PathParam("id") String id);

    @PUT
    @Path("{id}")
    Session update(@PathParam("id") String id, Session session);

    @DELETE
    @Path("{id}")
    Response remove(@PathParam("id") String id);

    @POST
    Session insert(Session session);

}


import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("conferences")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface ConferenceService {

    @GET
    List<Conference> findAll();

    @GET
    @Path("{id}")
    Conference findById(@PathParam("id") String id);

    @PUT
    @Path("{id}")
    Conference update(@PathParam("id") String id, Conference conference);

    @DELETE
    @Path("{id}")
    Response remove(@PathParam("id") String id);

    @POST
    Conference insert(Conference conference);
}

To make sure that the services are up, we conduct an Eclipse Microprofile Health check, so we can evaluate the HTTP status and the response time in milliseconds.

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;

import javax.ws.rs.client.Client;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

abstract class AbstractHealthCheck implements HealthCheck {


    abstract Client getClient();

    abstract String getUrl();

    abstract String getServiceName();

    @Override
    public HealthCheckResponse call() {
        try {
            long start = System.currentTimeMillis();
            Response response = getClient().target(getUrl()).request(MediaType.TEXT_PLAIN_TYPE)
                    .get();
            long end = System.currentTimeMillis() - start;
            return HealthCheckResponse.named(getServiceName())
                    .withData("service", "available")
                    .withData("time millis", end)
                    .withData("status", response.getStatus())
                    .withData("status", response.getStatusInfo().toEnum().toString())
                    .up()
                    .build();
        } catch (Exception exp) {
            return HealthCheckResponse.named(getServiceName())
                    .withData("services", "not available")
                    .down()
                    .build();
        }
    }
}

@Health
@ApplicationScoped
public class ConferenceHealthCheck extends AbstractHealthCheck {

    @Inject
    @ConfigProperty(name = "org.jespanol.client.conference.ConferenceService/mp-rest/url")
    private String url;

    private Client client;

    @PostConstruct
    public void init() {
        this.client = ClientBuilder.newClient();
    }


    @Override
    Client getClient() {
        return client;
    }

    @Override
    String getUrl() {
        return url;
    }

    @Override
    String getServiceName() {
        return "Conference Service";
    }
}


@Health
@ApplicationScoped
public class SessionHealthCheck extends AbstractHealthCheck {

    @Inject
    @ConfigProperty(name = "org.jespanol.client.session.SessionService/mp-rest/url")
    private String url;

    private Client client;

    @PostConstruct
    public void init() {
        this.client = ClientBuilder.newClient();
    }


    @Override
    Client getClient() {
        return client;
    }

    @Override
    String getUrl() {
        return url;
    }

    @Override
    String getServiceName() {
        return "Session Service";
    }
}


@Health
@ApplicationScoped
public class SpeakerHealthCheck extends AbstractHealthCheck {

    @Inject
    @ConfigProperty(name = "org.jespanol.client.speaker.SpeakerService/mp-rest/url")
    private String url;

    private Client client;

    @PostConstruct
    public void init() {
        this.client = ClientBuilder.newClient();
    }

    @Override
    Client getClient() {
        return client;
    }

    @Override
    String getUrl() {
        return url;
    }

    @Override
    String getServiceName() {
        return "Speaker Service";
    }
}

We can access the status at https://server_ip/health.

{
  "checks": [
    {
      "data": {
        "time millis": 11,
        "service": "available",
        "status": "OK"
      },
      "name": "Speaker Service",
      "state": "UP"
    },
    {
      "data": {
        "time millis": 11,
        "service": "available",
        "status": "OK"
      },
      "name": "Conference Service",
      "state": "UP"
    },
    {
      "data": {
        "time millis": 10,
        "service": "available",
        "status": "OK"
      },
      "name": "Session Service",
      "state": "UP"
    }
  ],
  "outcome": "UP",
  "status": "UP"
}

Once Services and health check are ready, let's go to the controllers. Eclipse Krazo is an API built on JAX-RS. Therefore, any Jakarta EE developer will feel at home when creating a Controller class.

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jespanol.client.session.SessionService;
import org.jespanol.client.speaker.SpeakerService;

import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.mvc.View;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import java.util.Optional;

@Controller
@Path("conference")
public class ConferenceController {
    @Inject
    private Models models;

    @Inject
    @RestClient
    private SessionService sessionService;

    @Inject
    @RestClient
    private ConferenceService conferenceService;

    @Inject
    @RestClient
    private SpeakerService speakerService;

    @GET
    @View("conference.html")
    public void home() {
        this.models.put("conferences", conferenceService.findAll());
    }


    @Path("add")
    @GET
    @View("conference-add.html")
    public void add() {
        this.models.put("conference", new Conference());
        this.models.put("speakers", speakerService.findAll());
        this.models.put("presentations", sessionService.findAll());
    }

    @Path("delete/{id}")
    @GET
    @View("conference.html")
    public void delete(@PathParam("id") String id) {
        conferenceService.remove(id);
        this.models.put("conferences", conferenceService.findAll());
    }

    @Path("edit/{id}")
    @GET
    @View("conference-add.html")
    public void edit(@PathParam("id") String id) {
        final Conference conference = Optional.ofNullable(conferenceService.findById(id))
                .orElse(new Conference());
        this.models.put("conference", conference);
        this.models.put("speakers", speakerService.findAll());
        this.models.put("presentations", sessionService.findAll());
    }

    @Path("add")
    @POST
    @View("conference.html")
    public void add(@BeanParam Conference conference) {
        conference.update(speakerService, sessionService);
        if (conference.isIdEmpty()) {
            conferenceService.insert(conference);
        } else {
            conferenceService.update(conference.getId(), conference);
        }
        this.models.put("conferences", conferenceService.findAll());
    }
}

The HTML5 resource and replacement are on Thymeleaf. We wrote about it in a previous post, Thymeleaf puts the Java instance information on the HTML5 template dynamically.

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
  <html>
    <head>
      <title>Latin America Conf (Session)</title>
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <link rel="stylesheet" href="/css/bootstrap.min.css" />
      <meta charset="UTF-8" />
    </head>
    <body>
      <div class="container">
        <h1>Conference</h1>
        <form
          th:action="@{/conference/add}"
          method="post"
          accept-charset="UTF-8"
        >
          <input id="id" name="id" type="hidden" th:value="${conference.id}" />
          <div class="form-group">
            <label for="conferenceName">Name</label>
            <input
              type="text"
              class="form-control"
              th:value="${conference.name}"
              name="name"
              id="conferenceName"
              placeholder="Enter Session Name"
              required
            />
          </div>
          <div class="form-group">
            <label for="conferenceCity">City</label>
            <input
              type="text"
              class="form-control"
              th:value="${conference.city}"
              name="city"
              id="conferenceCity"
              placeholder="Enter Conference City"
              required
            />
          </div>
          <div class="form-group">
            <label for="conferenceLink">Link</label>
            <input
              type="url"
              class="form-control"
              th:value="${conference.link}"
              name="link"
              id="conferenceLink"
              placeholder="Enter Conference Link"
              required
            />
          </div>
          <div class="form-group">
            <label for="conferenceYear">Year</label>
            <input
              type="number"
              class="form-control"
              th:value="${conference.year}"
              name="year"
              id="conferenceYear"
              placeholder="Enter Conference Year"
              required
            />
          </div>
          <div class="form-group">
            <label for="conferenceSpeakers">Speakers</label>
            <select
              class="form-control"
              id="conferenceSpeakers"
              th:value="${conference.speakersIds}"
              name="speakers"
              multiple
            >
              <tr th:each="speaker : ${speakers}">
                <option
                  th:value="${speaker.id}"
                  th:text="${speaker.name}"
                  th:selected="${conference.speakersIds.contains(speaker.id)}"
                ></option>
              </tr>
            </select>
          </div>
          <div class="form-group">
            <label for="conferenceSpeakers">Sessions</label>
            <select
              class="form-control"
              id="conferenceSessions"
              th:value="${conference.sessionsIds}"
              name="presentations"
              multiple
            >
              <tr th:each="presentation : ${presentations}">
                <option
                  th:value="${presentation.id}"
                  th:text="${presentation.name}"
                  th:selected="${conference.sessionsIds.contains(presentation.id)}"
                ></option>
              </tr>
            </select>
          </div>
          <button type="submit" class="btn">Save</button>
        </form>
      </div>
      <script src="https://code.jquery.com/jquery.js"></script>
      <script src="/js/bootstrap.min.js"></script>
    </body>
  </html>
</html>

Finally, the code

In this post, we finally saw some code and design, which is always exciting! Eclipse MicroProfile has a bright future integrated with Jakarta EE to allow Java developers to create several applications styles—like microservices and monoliths—that use either JPA or NoSQL. Besides that, these projects offer scalability and straightforward Java applications for your business. Platform.sh delivers on the promise of a NoOps pipeline for your Java microservices.

Get the latest Platform.sh news and resources
Subscribe

Related Content

Cover image

Installers that don't suck

Company
AboutSecurity and complianceTrust CenterBoard and investorsCareersPressContact us
Leader Winter 2023
System StatusPrivacyTerms of ServiceImpressumWCAG ComplianceManage your cookie preferencesReport a security issue
© 2023 Platform.sh. All rights reserved.
Supported by Horizon 2020's SME Instrument - European Commission 🇪🇺