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