1 /*
2  * Copyright 2015 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *       http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.cloud.examples.datastore;
18 
19 import com.google.cloud.Timestamp;
20 import com.google.cloud.datastore.Datastore;
21 import com.google.cloud.datastore.DatastoreOptions;
22 import com.google.cloud.datastore.Entity;
23 import com.google.cloud.datastore.FullEntity;
24 import com.google.cloud.datastore.IncompleteKey;
25 import com.google.cloud.datastore.Key;
26 import com.google.cloud.datastore.KeyFactory;
27 import com.google.cloud.datastore.Query;
28 import com.google.cloud.datastore.QueryResults;
29 import com.google.cloud.datastore.StructuredQuery;
30 import com.google.cloud.datastore.StructuredQuery.PropertyFilter;
31 import com.google.cloud.datastore.Transaction;
32 import java.util.Arrays;
33 import java.util.HashMap;
34 import java.util.Map;
35 import java.util.TreeMap;
36 
37 /**
38  * An example of using Google Cloud Datastore.
39  *
40  * <p>This example adds, displays or clears comments for a given user. This example also sets
41  * contact information for a user.
42  *
43  * <p>See the <a
44  * href="https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-examples/README.md">
45  * README</a> for compilation instructions. Run this code with
46  *
47  * <pre>{@code target/appassembler/bin/DatastoreExample
48  * [projectId] [user] [delete|display|add comment|set <email> <phone>]}</pre>
49  *
50  * <p>If no action is provided {@code display} is executed.
51  */
52 public class DatastoreExample {
53 
54   private static final String USER_KIND = "_DS_EXAMPLE_USER";
55   private static final String COMMENT_KIND = "_DS_EXAMPLE_COMMENT";
56   private static final String NAMESPACE = "google_cloud_example";
57   private static final String DEFAULT_ACTION = "display";
58   private static final Map<String, DatastoreAction> ACTIONS = new HashMap<>();
59 
60   private abstract static class DatastoreAction<T> {
61 
run(Transaction tx, Key userKey, T arg)62     abstract void run(Transaction tx, Key userKey, T arg) throws Exception;
63 
parse(String... args)64     abstract T parse(String... args) throws Exception;
65 
params()66     protected String params() {
67       return "";
68     }
69   }
70 
71   /**
72    * This class demonstrates how to delete a user. This action also queries the keys of all comments
73    * associated with the user and uses them to delete comments.
74    */
75   private static class DeleteAction extends DatastoreAction<Void> {
76     @Override
run(Transaction tx, Key userKey, Void arg)77     public void run(Transaction tx, Key userKey, Void arg) {
78       Entity user = tx.get(userKey);
79       if (user == null) {
80         System.out.println("Nothing to delete, user does not exist.");
81         return;
82       }
83       Query<Key> query =
84           Query.newKeyQueryBuilder()
85               .setNamespace(NAMESPACE)
86               .setKind(COMMENT_KIND)
87               .setFilter(PropertyFilter.hasAncestor(userKey))
88               .build();
89       QueryResults<Key> comments = tx.run(query);
90       int count = 0;
91       while (comments.hasNext()) {
92         tx.delete(comments.next());
93         count++;
94       }
95       tx.delete(userKey);
96       System.out.printf("Deleting user '%s' and %d comment[s].%n", userKey.getName(), count);
97     }
98 
99     @Override
parse(String... args)100     Void parse(String... args) throws Exception {
101       return null;
102     }
103   }
104 
105   /**
106    * This class demonstrates how to get a user. The action also queries all comments associated with
107    * the user.
108    */
109   private static class DisplayAction extends DatastoreAction<Void> {
110     @Override
run(Transaction tx, Key userKey, Void arg)111     public void run(Transaction tx, Key userKey, Void arg) {
112       Entity user = tx.get(userKey);
113       if (user == null) {
114         System.out.printf("User '%s' does not exist.%n", userKey.getName());
115         return;
116       }
117       if (user.contains("contact")) {
118         FullEntity<IncompleteKey> contact = user.getEntity("contact");
119         String email = contact.getString("email");
120         String phone = contact.getString("phone");
121         System.out.printf(
122             "User '%s' email is '%s', phone is '%s'.%n", userKey.getName(), email, phone);
123       }
124       System.out.printf("User '%s' has %d comment[s].%n", userKey.getName(), user.getLong("count"));
125       int limit = 200;
126       Map<Timestamp, String> sortedComments = new TreeMap<>();
127       StructuredQuery<Entity> query =
128           Query.newEntityQueryBuilder()
129               .setNamespace(NAMESPACE)
130               .setKind(COMMENT_KIND)
131               .setFilter(PropertyFilter.hasAncestor(userKey))
132               .setLimit(limit)
133               .build();
134       while (true) {
135         QueryResults<Entity> results = tx.run(query);
136         int resultCount = 0;
137         while (results.hasNext()) {
138           Entity result = results.next();
139           sortedComments.put(result.getTimestamp("timestamp"), result.getString("content"));
140           resultCount++;
141         }
142         if (resultCount < limit) {
143           break;
144         }
145         query = query.toBuilder().setStartCursor(results.getCursorAfter()).build();
146       }
147       // We could have added "ORDER BY timestamp" to the query to avoid sorting, but that would
148       // require adding an ancestor index for timestamp.
149       // See: https://cloud.google.com/datastore/docs/tools/indexconfig
150       for (Map.Entry<Timestamp, String> entry : sortedComments.entrySet()) {
151         System.out.printf("\t%s: %s%n", entry.getKey(), entry.getValue());
152       }
153     }
154 
155     @Override
parse(String... args)156     Void parse(String... args) throws Exception {
157       return null;
158     }
159   }
160 
161   /** This class adds a comment for a user. If the user does not exist its entity is created. */
162   private static class AddCommentAction extends DatastoreAction<String> {
163     @Override
run(Transaction tx, Key userKey, String content)164     public void run(Transaction tx, Key userKey, String content) {
165       Entity user = tx.get(userKey);
166       if (user == null) {
167         System.out.println("Adding a new user.");
168         user = Entity.newBuilder(userKey).set("count", 1).build();
169         tx.add(user);
170       } else {
171         user = Entity.newBuilder(user).set("count", user.getLong("count") + 1L).build();
172         tx.update(user);
173       }
174       IncompleteKey commentKey = IncompleteKey.newBuilder(userKey, COMMENT_KIND).build();
175       FullEntity<IncompleteKey> comment =
176           FullEntity.newBuilder(commentKey)
177               .set("content", content)
178               .set("timestamp", Timestamp.now())
179               .build();
180       tx.addWithDeferredIdAllocation(comment);
181       System.out.printf("Adding a comment to user '%s'.%n", userKey.getName());
182     }
183 
184     @Override
parse(String... args)185     String parse(String... args) throws Exception {
186       String content = "No comment.";
187       if (args.length > 0) {
188         StringBuilder stBuilder = new StringBuilder();
189         for (String arg : args) {
190           stBuilder.append(arg).append(' ');
191         }
192         stBuilder.setLength(stBuilder.length() - 1);
193         content = stBuilder.toString();
194       }
195       return content;
196     }
197 
198     @Override
params()199     protected String params() {
200       return "<comment>";
201     }
202   }
203 
204   /**
205    * This class sets contact information (email and phone) for a user. If the user does not exist
206    * its entity is created. Contact information is saved as an entity embedded in the user entity.
207    */
208   private static class SetContactAction extends DatastoreAction<SetContactAction.Contact> {
209 
210     static final class Contact {
211 
212       private final String email;
213       private final String phone;
214 
Contact(String email, String phone)215       Contact(String email, String phone) {
216         this.email = email;
217         this.phone = phone;
218       }
219 
email()220       String email() {
221         return email;
222       }
223 
phone()224       String phone() {
225         return phone;
226       }
227     }
228 
229     @Override
run(Transaction tx, Key userKey, Contact contact)230     public void run(Transaction tx, Key userKey, Contact contact) {
231       Entity user = tx.get(userKey);
232       if (user == null) {
233         System.out.println("Adding a new user.");
234         user = Entity.newBuilder(userKey).set("count", 0L).build();
235         tx.add(user);
236       }
237       FullEntity<IncompleteKey> contactEntity =
238           FullEntity.newBuilder()
239               .set("email", contact.email())
240               .set("phone", contact.phone())
241               .build();
242       tx.update(Entity.newBuilder(user).set("contact", contactEntity).build());
243       System.out.printf("Setting contact for user '%s'.%n", userKey.getName());
244     }
245 
246     @Override
parse(String... args)247     Contact parse(String... args) throws Exception {
248       String message;
249       if (args.length == 2) {
250         return new Contact(args[0], args[1]);
251       } else if (args.length > 2) {
252         message = "Too many arguments.";
253       } else {
254         message = "Missing required email and phone.";
255       }
256       throw new IllegalArgumentException(message);
257     }
258 
259     @Override
params()260     protected String params() {
261       return "<email> <phone>";
262     }
263   }
264 
265   static {
266     ACTIONS.put("delete", new DeleteAction());
267     ACTIONS.put("add", new AddCommentAction());
268     ACTIONS.put("set", new SetContactAction());
269     ACTIONS.put("display", new DisplayAction());
270   }
271 
printUsage()272   private static void printUsage() {
273     StringBuilder actionAndParams = new StringBuilder();
274     for (Map.Entry<String, DatastoreAction> entry : ACTIONS.entrySet()) {
275       actionAndParams.append("\n\t").append(entry.getKey());
276 
277       String param = entry.getValue().params();
278       if (param != null && !param.isEmpty()) {
279         actionAndParams.append(' ').append(param);
280       }
281     }
282     System.out.printf(
283         "Usage: %s <project_id> <user> operation <args>*%s%n",
284         DatastoreExample.class.getSimpleName(), actionAndParams);
285   }
286 
287   @SuppressWarnings("unchecked")
main(String... args)288   public static void main(String... args) throws Exception {
289     String projectId = args.length > 0 ? args[0] : null;
290     // If you want to access a local Datastore running via the Google Cloud SDK, do
291     //   DatastoreOptions options = DatastoreOptions.newBuilder()
292     //       .setProjectId(projectId)
293     //       .setNamespace(NAMESPACE)
294     //       // change 8080 to the port that the emulator listens to
295     //       .setHost("http://localhost:8080")
296     //       .build();
297     DatastoreOptions options =
298         DatastoreOptions.newBuilder().setProjectId(projectId).setNamespace(NAMESPACE).build();
299     String name = args.length > 1 ? args[1] : System.getProperty("user.getName");
300     Datastore datastore = options.getService();
301     KeyFactory keyFactory = datastore.newKeyFactory().setKind(USER_KIND);
302     Key key = keyFactory.newKey(name);
303     String actionName = args.length > 2 ? args[2].toLowerCase() : DEFAULT_ACTION;
304     DatastoreAction action = ACTIONS.get(actionName);
305     if (action == null) {
306       System.out.println("Unrecognized action.");
307       printUsage();
308       return;
309     }
310     args = args.length > 3 ? Arrays.copyOfRange(args, 3, args.length) : new String[] {};
311     Transaction tx = datastore.newTransaction();
312     Object request;
313     try {
314       request = action.parse(args);
315     } catch (IllegalArgumentException ex) {
316       System.out.printf("Invalid input for action '%s'. %s%n", actionName, ex.getMessage());
317       System.out.printf("Expected: %s%n", action.params());
318       return;
319     } catch (Exception ex) {
320       System.out.println("Failed to parse request.");
321       ex.printStackTrace();
322       return;
323     }
324     try {
325       action.run(tx, key, request);
326       tx.commit();
327     } finally {
328       if (tx.isActive()) {
329         tx.rollback();
330       }
331     }
332   }
333 }
334