Get started with Java

This tutorial is intended for those new to building apps in the cloud, such as engineers and web developers, who want to learn key app development concepts as they apply to Cloud de Confiance by S3NS.

Deploying your app to Cloud Run

Cloud de Confiance offers several options for running your code. For this example, you use Cloud Run to deploy a scalable app to Cloud de Confiance. Cloud Run doesn't require you to manage servers and automatically scales to support traffic spikes.

Follow the instructions at Deploying your app to Cloud Run.

When the deployment succeeds, it outputs an endpoint to the app running in Cloud Run, in the following format:

https://bookshelf-abcdefghij-uc.a.run.app

Your app is now viewable at this link, hereafter called YOUR_CLOUD_RUN_URL. In your web browser, enter the URL to view the app.

Bookshelf app homepage

Persist your data with Firestore

You cannot store information on your Cloud Run instances, because it is lost if the instance is restarted, and doesn't exist when new instances are created. Instead, you use a database that all your instances read from and write to.

Cloud de Confiance offers several options for storing your data. In this example, you use Firestore to store the data for each book. Firestore is a fully managed, serverless, NoSQL document database that lets you store and query data. Firestore auto scales to meet your app needs, and scales to zero when you're not using it. Add your first book now.

  1. In your web browser, go to your YOUR_CLOUD_RUN_URL.
  2. To create a book for your deployed app, click Add book.

    Add a book to the Bookshelf app
  3. In the Title field, enter Moby Dick.
  4. In the Author field, enter Herman Melville.
  5. Click Save. There is now an entry to your Bookshelf app.

    Moby Dick Bookshelf app entry
  6. In the Cloud de Confiance console, to refresh the Firestore page, click Refresh . The data appears in Firestore. The Bookshelf app stores each book as a Firestore document with a unique ID, and all these documents are stored in a Firestore collection. For the purposes of this tutorial, the collection is called books. Example of a Firestore document.

Firestore stores the books by using the Firestore Client Library. Here is an example of fetching a Firestore document:

public class FirestoreDao implements BookDao {
  private CollectionReference booksCollection;

  public FirestoreDao() {
    Firestore firestore = FirestoreOptions.getDefaultInstance().getService();
    booksCollection = firestore.collection("books");
  }

  private Book documentToBook(DocumentSnapshot document) {
    Map<String, Object> data = document.getData();
    if (data == null) {
      System.out.println("No data in document " + document.getId());
      return null;
    }

    return new Book.Builder()
        .author((String) data.get(Book.AUTHOR))
        .description((String) data.get(Book.DESCRIPTION))
        .publishedDate((String) data.get(Book.PUBLISHED_DATE))
        .imageUrl((String) data.get(Book.IMAGE_URL))
        .createdBy((String) data.get(Book.CREATED_BY))
        .createdById((String) data.get(Book.CREATED_BY_ID))
        .title((String) data.get(Book.TITLE))
        .id(document.getId())
        .build();
  }

  @Override
  public String createBook(Book book) {
    String id = UUID.randomUUID().toString();
    DocumentReference document = booksCollection.document(id);
    Map<String, Object> data = Maps.newHashMap();

    data.put(Book.AUTHOR, book.getAuthor());
    data.put(Book.DESCRIPTION, book.getDescription());
    data.put(Book.PUBLISHED_DATE, book.getPublishedDate());
    data.put(Book.TITLE, book.getTitle());
    data.put(Book.IMAGE_URL, book.getImageUrl());
    data.put(Book.CREATED_BY, book.getCreatedBy());
    data.put(Book.CREATED_BY_ID, book.getCreatedById());
    try {
      document.set(data).get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }

    return id;
  }

  @Override
  public Book readBook(String bookId) {
    try {
      DocumentSnapshot document = booksCollection.document(bookId).get().get();

      return documentToBook(document);
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
    return null;
  }

  @Override
  public void updateBook(Book book) {
    DocumentReference document = booksCollection.document(book.getId());
    Map<String, Object> data = Maps.newHashMap();

    data.put(Book.AUTHOR, book.getAuthor());
    data.put(Book.DESCRIPTION, book.getDescription());
    data.put(Book.PUBLISHED_DATE, book.getPublishedDate());
    data.put(Book.TITLE, book.getTitle());
    data.put(Book.IMAGE_URL, book.getImageUrl());
    data.put(Book.CREATED_BY, book.getCreatedBy());
    data.put(Book.CREATED_BY_ID, book.getCreatedById());
    try {
      document.set(data).get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void deleteBook(String bookId) {
    try {
      booksCollection.document(bookId).delete().get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
  }

  private List<Book> documentsToBooks(List<QueryDocumentSnapshot> documents) {
    List<Book> resultBooks = new ArrayList<>();
    for (QueryDocumentSnapshot snapshot : documents) {
      resultBooks.add(documentToBook(snapshot));
    }
    return resultBooks;
  }

  @Override
  public Result<Book> listBooks(String startTitle) {
    Query booksQuery = booksCollection.orderBy("title").limit(10);
    if (startTitle != null) {
      booksQuery = booksQuery.startAfter(startTitle);
    }
    try {
      QuerySnapshot snapshot = booksQuery.get().get();
      List<Book> results = documentsToBooks(snapshot.getDocuments());
      String newCursor = null;
      if (results.size() > 0) {
        newCursor = results.get(results.size() - 1).getTitle();
      }
      return new Result<>(results, newCursor);
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
    return new Result<>(Lists.newArrayList(), null);
  }

  @Override
  public Result<Book> listBooksByUser(String userId, String startTitle) {
    Query booksQuery =
        booksCollection.orderBy("title").whereEqualTo(Book.CREATED_BY_ID, userId).limit(10);
    if (startTitle != null) {
      booksQuery = booksQuery.startAfter(startTitle);
    }
    try {
      QuerySnapshot snapshot = booksQuery.get().get();
      List<Book> results = documentsToBooks(snapshot.getDocuments());
      String newCursor = null;
      if (results.size() > 0) {
        newCursor = results.get(results.size() - 1).getTitle();
      }
      return new Result<>(results, newCursor);
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
    return new Result<>(Lists.newArrayList(), null);
  }
}

For more information on using Firestore, see Adding data to Firestore.

Store file uploads in Cloud Storage

Now that you've added a book, it's time to add the book cover image. You cannot store files on your instances. A database isn't the right choice for image files. Instead, you use Cloud Storage.

Cloud Storage is the primary blob store for Cloud de Confiance. You can use Cloud Storage to host app assets that you want to share across Cloud de Confiance. To use Cloud Storage, you need to create a Cloud Storage bucket, a basic container to hold your data.

  1. In the Cloud de Confiance console, go to the Cloud Storage Browser page.

    Go to the Cloud Storage Browser page

  2. Click Create bucket.
  3. In the Create bucket dialog, enter a name for your bucket by appending your Cloud de Confiance project ID to the string _bucket so the name looks like YOUR_PROJECT_ID_bucket. This name is subject to the bucket name requirements. All other fields can remain at their default values.
  4. Click Create.
  5. After your bucket is created, objects must be made publicly accessible to be viewed by users. To make your objects publicly accessible see Making Data Public.
  6. Click Edit book, and select an image to upload as your book's cover. For example, you can use this public domain image:
    Moby Dick book cover
  7. Click Save. You're redirected to the homepage, where there is an entry to your Bookshelf app.
    Moby Dick Bookshelf app entry

The bookshelf app sends uploaded files to Cloud Storage by using the Cloud Storage Client Library.

public class CloudStorageHelper {

  private final Logger logger = Logger.getLogger(CloudStorageHelper.class.getName());
  private static Storage storage = null;

  static {
    storage = StorageOptions.getDefaultInstance().getService();
  }


  /**
   * Uploads a file to Google Cloud Storage to the bucket specified in the BUCKET_NAME environment
   * variable, appending a timestamp to end of the uploaded filename.
   */
  public String uploadFile(FileItemStream fileStream, final String bucketName)
      throws IOException, ServletException {
    checkFileExtension(fileStream.getName());

    System.out.println("FileStream name: " + fileStream.getName() + "\nBucket name: " + bucketName);

    DateTimeFormatter dtf = DateTimeFormat.forPattern("-YYYY-MM-dd-HHmmssSSS");
    DateTime dt = DateTime.now(DateTimeZone.UTC);
    String dtString = dt.toString(dtf);
    final String fileName = fileStream.getName() + dtString;

    // the inputstream is closed by default, so we don't need to close it here
    @SuppressWarnings("deprecation")
    BlobInfo blobInfo =
        storage.create(
            BlobInfo.newBuilder(bucketName, fileName)
                // Modify access list to allow all users with link to read file
                .setAcl(new ArrayList<>(Arrays.asList(Acl.of(User.ofAllUsers(), Role.READER))))
                .build(),
            fileStream.openStream());
    logger.log(
        Level.INFO, "Uploaded file {0} as {1}", new Object[] {fileStream.getName(), fileName});
    // return the public download link
    return blobInfo.getMediaLink();
  }


  /** Checks that the file extension is supported. */
  private void checkFileExtension(String fileName) throws ServletException {
    if (fileName != null && !fileName.isEmpty() && fileName.contains(".")) {
      String[] allowedExt = {".jpg", ".jpeg", ".png", ".gif"};
      for (String ext : allowedExt) {
        if (fileName.endsWith(ext)) {
          return;
        }
      }
      throw new ServletException("file must be an image");
    }
  }
}

For more information on using Cloud Storage, see the Cloud Storage introduction.

Monitor your app using Google Cloud Observability

You've deployed your app and created and modified books. To monitor these events for your users, use Application Performance Management.

Monitor logs with Cloud Logging

  1. In the Cloud de Confiance, go to the Logs Explorer

    Go to Logs Explorer

    You can monitor your app in real time. If you have any issues with your app, this is one of the first places to look.

    Stackdriver Log Viewer
  2. In the Resource drop-down list, select Cloud Run Revision, bookshelf.

Monitor errors with Error Reporting

  1. In the Cloud de Confiance console, go to the Error Reporting page.
    Go to Error Reporting page
    Error Reporting highlights errors and exceptions in your app and lets you set up alerting around them.
  2. In your browser, go to the /errors URL in your app.
    YOUR_CLOUD_RUN_URL/errors

    This generates a new test exception and sends it to Google Cloud Observability.

  3. In the Cloud de Confiance console, return to the Error Reporting page, and in a few moments the new error is visible. Click Auto Reload so you don't need to manually refresh the page.

    Error message from Error Reporting.