Objectify

Objectify is a Java API created for Datastore, a NoSQL database created by Google, and the official repository of Google Cloud.

How to use Objectify

Add the following to the dependencies in pom.xml.

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>23.0</version>
</dependency>
<dependency>
  <groupId>com.googlecode.objectify</groupId>
  <artifactId>objectify</artifactId>
  <version>5.1.22</version>
</dependency>

Create a helper class as belows.

package net.java_school.guestbook;

//..Omit..

public class OfyHelper implements ServletContextListener {
  public static void register() {
    ObjectifyService.register(Guestbook.class);
    ObjectifyService.register(Greeting.class);
  }

  public void contextInitialized(ServletContextEvent event) {
    //This will be invoked as part of a warmup request, or the first user
    //request if no warmup request was invoked.
    register();
  }

  public void contextDestroyed(ServletContextEvent event) {
    //App Engine does not currently invoke this method.
  }
}

Add the following to the web.xml.

<!-- [START Objectify] -->
<filter>
  <filter-name>ObjectifyFilter</filter-name>
  <filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>ObjectifyFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
  <listener-class>net.java_school.guestbook.OfyHelper</listener-class>
</listener>
<!-- [END Objectify] -->

The guestbook archetype uses Objectify, so it already has the above configuration.

Delete Greetings

Let's look at the Objectify code snippets implemented in the guestbook.jsp to add new code.

Fetch List
List<Greeting> greetings = ObjectifyService.ofy()
  .load()
  .type(Greeting.class) //We want only Greetings
  .ancestor(theBook)    //Anyone in this book
  .order("-date")       //Most recent first - date is indexed.
  .limit(5)             //Only show 5 of them.
  .list();
Save Entity
ObjectifyService.ofy().save().entity(greeting).now();

The followings are the code to add.

Delete Entity
ObjectifyService.ofy().delete().key(key).now();

Here, the key is the unique key of the entity. You can obtain the unique key with the following code.

Key.create(theBook, Greeting.class, id);

With the above code, you can get the unique key of each Greeting object in the guestbook.jsp. Add the following method to the Greeting class to make getting the key easy:

public Key<Greeting> getKey() {
  return Key.create(theBook, Greeting.class, id);
}

The type of Key is com.googlecode.objectify.Key. Key has toWebSafeString() method that returns a string with which you can restore Key.

Add code to the guestbook.jsp like below.

// Look at all of our greetings
for (Greeting greeting : greetings) {
  pageContext.setAttribute("greeting_content", greeting.content);
  pageContext.setAttribute("keyString", greeting.getKey().toWebSafeString());
	
  //..omit..

Using the keyString stored in pageContext, you can create a link that passes the keyString to the JavaScript function.

Add code to the guestbook.jsp like below.

<p><b>${fn:escapeXml(greeting_user)}</b> wrote:</p>
<blockquote>${fn:escapeXml(greeting_content)}</blockquote>
<blockquote><a href="javascript:del('${keyString }')">Del</a></blockquote>

The above code allows non-author users or even non-logged users to delete posts. Spring security tags will enable you to render views based on permissions selectively. To compare a login user with an author, you need to store the author ID in the pageContext.

Modify the guestbook.jsp file like below.

// Look at all of our greetings
for (Greeting greeting : greetings) {
  pageContext.setAttribute("greeting_content", greeting.content);
  pageContext.setAttribute("keyString", greeting.getKey().toWebSafeString());
  String author;
  String author_id = null;
  if (greeting.author_email == null) {
      author = "An anonymous person";
  } else {
      author = greeting.author_email;
      author_id = greeting.author_id;
      if (user != null && user.getUserId().equals(author_id)) {
          author += " (You)";
      }
  }
  pageContext.setAttribute("greeting_user", author);
  pageContext.setAttribute("author_id", author_id);
    
  //..omit..

You are all ready to compare the login user with the author.

Add the code to the guestbook.jsp like below.

<p><b>${fn:escapeXml(greeting_user)}</b> wrote:</p>
<blockquote>${fn:escapeXml(greeting_content)}</blockquote>
<security:authorize access="isAuthenticated() and (#author_id == principal.userId or hasRole('ROLE_ADMIN'))">
  <blockquote><a href="javascript:del('${keyString }')">Del</a></blockquote>
</security:authorize>

To use the Spring Security tag, add the Spring Security tag library directive to the guestbook.jsp file.

<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>

Add the following JavaScript function to the guestbook.jsp.

<script type="text/javascript">
function del(key) {
  var check = confirm('Are you sure you want to delete this greeting?'); 
  if (check) {
    var form = document.getElementById("delForm");
    form.keyString.value = key;
    form.submit();
  }
}
</script>

Add the following form to the guestbook.jsp.

<form id="delForm" action="/guestbook/del" method="post" style="display: none;">
  <input type="hidden" name="keyString" />
  <input type="hidden" name="guestbookName" value="${fn:escapeXml(guestbookName)}"/>
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>

GuestbookController needs a handler for the "/guestbook/del" request.

@PostMapping("/guestbook/del")
public String del(String guestbookName, String keyString) {
  Key<Greeting> key = Key.create(keyString);
  ObjectifyService.ofy().delete().key(key).now();
  return "redirect:/guestbook/?guestbookName=" + guestbookName;
}	

The above code only verifies the user on the client side. You can further enhance security by verifying users on the server-side as well. Method security in Spring Security lets you verify that the login user is the author on the server-side. Spring official documentation recommends implementing method security in the service layer.

Create a service layer as shown below.

GuestbookService.java
package net.java_school.guestbook;

import org.springframework.stereotype.Service;

@Service
public interface GuestbookService {
  public void sign(Greeting greeting);
  public void del(Greeting greeting);
}
GuestbookServiceImpl.java
package net.java_school.guestbook;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

import com.googlecode.objectify.Key;
import static com.googlecode.objectify.ObjectifyService.ofy;//1.

@Service
public class GuestbookServiceImpl implements GuestbookService{
  public void sign(Greeting greeting) {
    ofy().save().entity(greeting).now();//1.
  }

  @PreAuthorize("isAuthenticated() and (#greeting.author_id == principal.userId or hasRole('ROLE_ADMIN'))")//2.
  public void del(Greeting greeting) {
    Key<Greeting> key = greeting.getKey();
    ofy().delete().key(key).now();//1.
  }
}

1. You need import static com.googlecode.objectify.ObjectifyService.ofy; to use Objectify codes like below:

ofy().save().entity(greeting).now();
ofy().delete().key(key).now();

2. In the annotation of the del() method, #greeting.author_id calls the getAuthor_id() method of the greeting instance. So you need to add the following getter to Greeting.java:

Greeting.java
public String getAuthor_id() {
  return author_id;
}

Modify the GuestbookController to use the service layer.

GuestbookController.java
//omit
import net.java_school.spring.security.GaeUserAuthentication;
import net.java_school.user.GaeUser;
import static com.googlecode.objectify.ObjectifyService.ofy;
//omit

@Autowired
private GuestbookService guestbookService;

//omit

@PostMapping("/guestbook/sign")
public String sign(String guestbookName, String content, GaeUserAuthentication gaeUserAuthentication) {
  Greeting greeting = null;

  if (gaeUserAuthentication != null) {
    GaeUser gaeUser = (GaeUser) gaeUserAuthentication.getPrincipal();
    greeting = new Greeting(guestbookName, content, gaeUser.getUserId(), gaeUser.getEmail());
  } else {
    greeting = new Greeting(guestbookName, content);
  }

  guestbookService.sign(greeting);

  return "redirect:/guestbook/?guestbookName=" + guestbookName;
}

@PostMapping("/guestbook/del")
public String del(String guestbookName, String keyString) {
  Key<Greeting> key = Key.create(keyString);
  Greeting greeting = ofy().load().key(key).now();
  guestbookService.del(greeting);
  return "redirect:/guestbook/?guestbookName=" + guestbookName;
}

Local Test

mvn clean
mvn appengine:run

When not logged users visit the guestbook, they can not see the Del link.

not login

Log in as a regular user.

Normal user login screen

User can see links to delete his posts.

Normal user login

Log out and log in as an administrator.

Admin user login screen

An administrator can see the Delete link of all posts. Delete an article written by an anonymous user.

Admin login

Confirm that the guestbook system has deleted the article written by the anonymous user.

delete Anomynous user's greeting

References