001/* 002 * (C) Copyright 2016-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Thomas Roger 018 * Yannis JULIENNE 019 * Kevin Leturc <[email protected]> 020 */ 021package org.nuxeo.functionaltests; 022 023import static org.nuxeo.functionaltests.AbstractTest.NUXEO_URL; 024import static org.nuxeo.functionaltests.Constants.ADMINISTRATOR; 025 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.function.Supplier; 035 036import org.apache.commons.lang3.StringUtils; 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.apache.http.HttpStatus; 040import org.nuxeo.client.NuxeoClient; 041import org.nuxeo.client.objects.Document; 042import org.nuxeo.client.objects.Documents; 043import org.nuxeo.client.objects.acl.ACE; 044import org.nuxeo.client.objects.directory.DirectoryEntry; 045import org.nuxeo.client.objects.operation.DocRef; 046import org.nuxeo.client.objects.user.Group; 047import org.nuxeo.client.objects.user.User; 048import org.nuxeo.client.objects.workflow.Workflow; 049import org.nuxeo.client.objects.workflow.Workflows; 050import org.nuxeo.client.spi.NuxeoClientRemoteException; 051 052import okhttp3.Response; 053 054/** 055 * @since 8.3 056 */ 057public class RestHelper { 058 059 private static final NuxeoClient CLIENT = new NuxeoClient.Builder().url(NUXEO_URL) 060 .authentication(ADMINISTRATOR, ADMINISTRATOR) 061 // by default timeout is 10s, hot reload needs a 062 // bit more 063 .timeout(120) 064 .connect(); 065 066 private static final String USER_WORKSPACE_PATH_FORMAT = "/default-domain/UserWorkspaces/%s"; 067 068 private static final String DEFAULT_USER_EMAIL = "[email protected]"; 069 070 private static final String DOCUMENT_QUERY_BY_PATH_BASE = "SELECT * FROM Document WHERE ecm:path = '%s'"; 071 072 /** 073 * Documents to delete in cleanup step. Key is the document id and value is its path. 074 * 075 * @since 9.3 076 */ 077 private static final Map<String, String> documentsToDelete = new HashMap<>(); 078 079 private static final List<String> usersToDelete = new ArrayList<>(); 080 081 private static final List<String> groupsToDelete = new ArrayList<>(); 082 083 protected static final Map<String, Set<String>> directoryEntryIdsToDelete = new HashMap<>(); 084 085 protected static final Log log = LogFactory.getLog(RestHelper.class); 086 087 private RestHelper() { 088 // helper class 089 } 090 091 public static void cleanup() { 092 cleanupDocuments(); 093 cleanupUsers(); 094 cleanupGroups(); 095 cleanupDirectoryEntries(); 096 } 097 098 public static void cleanupDocuments() { 099 // delete by ids 100 documentsToDelete.keySet().forEach(RestHelper::deleteDocument); 101 documentsToDelete.clear(); 102 } 103 104 public static void cleanupUsers() { 105 for (String user : usersToDelete) { 106 RestHelper.deleteDocument(String.format(USER_WORKSPACE_PATH_FORMAT, user)); 107 } 108 usersToDelete.forEach(RestHelper::deleteUser); 109 usersToDelete.clear(); 110 } 111 112 public static void cleanupGroups() { 113 groupsToDelete.forEach(RestHelper::deleteGroup); 114 groupsToDelete.clear(); 115 } 116 117 public static void cleanupDirectoryEntries() { 118 directoryEntryIdsToDelete.forEach( 119 (directoryName, entryIds) -> entryIds.forEach(id -> deleteDirectoryEntry(directoryName, id))); 120 clearDirectoryEntryIdsToDelete(); 121 } 122 123 /** 124 * @since 9.3 125 */ 126 public static void addDocumentToDelete(String idOrPath) { 127 Document document = fetchDocumentByIdOrPath(idOrPath); 128 addDocumentToDelete(document.getId(), document.getPath()); 129 } 130 131 /** 132 * @since 9.3 133 */ 134 public static void addDocumentToDelete(String id, String path) { 135 // do we already have to delete one parent? 136 if (documentsToDelete.values().stream().noneMatch(path::startsWith)) { 137 documentsToDelete.put(id, path); 138 } 139 } 140 141 /** 142 * @since 9.3 143 */ 144 public static void removeDocumentToDelete(String idOrPath) { 145 if (idOrPath.startsWith("/")) { 146 documentsToDelete.values().remove(idOrPath); 147 } else { 148 documentsToDelete.remove(idOrPath); 149 } 150 } 151 152 public static void addUserToDelete(String userName) { 153 usersToDelete.add(userName); 154 } 155 156 public static void removeUserToDelete(String userName) { 157 usersToDelete.remove(userName); 158 } 159 160 public static void addGroupToDelete(String groupName) { 161 groupsToDelete.add(groupName); 162 } 163 164 public static void addDirectoryEntryToDelete(String directoryName, String entryId) { 165 directoryEntryIdsToDelete.computeIfAbsent(directoryName, k -> new HashSet<>()).add(entryId); 166 } 167 168 /** 169 * @since 9.10 170 */ 171 public static void removeDirectoryEntryToDelete(String directoryName, String entryId) { 172 directoryEntryIdsToDelete.getOrDefault(directoryName, Collections.emptySet()).remove(entryId); 173 } 174 175 public static void clearDirectoryEntryIdsToDelete() { 176 directoryEntryIdsToDelete.clear(); 177 } 178 179 // --------------------- 180 // User & Group Services 181 // --------------------- 182 183 public static String createUser(String username, String password) { 184 return createUser(username, password, null, null, null, null, null); 185 } 186 187 public static String createUser(String username, String password, String firstName, String lastName, String company, 188 String email, String group) { 189 190 String finalEmail = StringUtils.isBlank(email) ? DEFAULT_USER_EMAIL : email; 191 192 User user = new User(); 193 user.setUserName(username); 194 user.setPassword(password); 195 user.setFirstName(firstName); 196 user.setLastName(lastName); 197 user.setCompany(company); 198 user.setEmail(finalEmail); 199 if (StringUtils.isNotBlank(group)) { 200 user.setGroups(Collections.singletonList(group)); 201 } 202 203 user = CLIENT.userManager().createUser(user); 204 205 String userId = user.getId(); 206 usersToDelete.add(userId); 207 return userId; 208 } 209 210 public static void deleteUser(String username) { 211 try { 212 CLIENT.userManager().deleteUser(username); 213 } catch (NuxeoClientRemoteException e) { 214 if (e.getStatus() == HttpStatus.SC_NOT_FOUND) { 215 log.warn(String.format("User %s not deleted because not found", username)); 216 } else { 217 throw e; 218 } 219 } 220 } 221 222 /** 223 * @since 9.3 224 */ 225 public static boolean userExists(String username) { 226 return exists(() -> CLIENT.userManager().fetchUser(username)); 227 } 228 229 public static void createGroup(String name, String label) { 230 createGroup(name, label, null, null); 231 } 232 233 public static void createGroup(String name, String label, String[] members, String[] subGroups) { 234 Group group = new Group(); 235 group.setGroupName(name); 236 group.setGroupLabel(label); 237 if (members != null) { 238 group.setMemberUsers(Arrays.asList(members)); 239 } 240 if (subGroups != null) { 241 group.setMemberGroups(Arrays.asList(subGroups)); 242 } 243 244 CLIENT.userManager().createGroup(group); 245 groupsToDelete.add(name); 246 } 247 248 public static void deleteGroup(String name) { 249 try { 250 CLIENT.userManager().deleteGroup(name); 251 } catch (NuxeoClientRemoteException e) { 252 if (e.getStatus() == HttpStatus.SC_NOT_FOUND) { 253 log.warn(String.format("Group %s not deleted because not found", name)); 254 } else { 255 throw e; 256 } 257 } 258 } 259 260 /** 261 * @since 9.3 262 */ 263 public static boolean groupExists(String groupName) { 264 return exists(() -> CLIENT.userManager().fetchGroup(groupName)); 265 } 266 267 // ----------------- 268 // Document Services 269 // ----------------- 270 271 /** 272 * @since 9.3 273 */ 274 public static String createDocument(String idOrPath, String type, String title) { 275 return createDocument(idOrPath, type, title, Collections.emptyMap()); 276 } 277 278 public static String createDocument(String idOrPath, String type, String title, String description) { 279 Map<String, Object> props; 280 if (description == null) { 281 props = Collections.emptyMap(); 282 } else { 283 props = Collections.singletonMap("dc:description", description); 284 } 285 return createDocument(idOrPath, type, title, props); 286 } 287 288 /** 289 * @since 9.3 290 */ 291 public static String createDocument(String idOrPath, String type, String title, Map<String, Object> props) { 292 Document document = Document.createWithName(title, type); 293 Map<String, Object> properties = new HashMap<>(); 294 if (props != null) { 295 properties.putAll(props); 296 } 297 properties.put("dc:title", title); 298 document.setProperties(properties); 299 300 if (idOrPath.startsWith("/")) { 301 document = CLIENT.repository().createDocumentByPath(idOrPath, document); 302 } else { 303 document = CLIENT.repository().createDocumentById(idOrPath, document); 304 } 305 306 String docId = document.getId(); 307 String docPath = document.getPath(); 308 addDocumentToDelete(docId, docPath); 309 return docId; 310 } 311 312 public static void deleteDocument(String idOrPath) { 313 if (idOrPath.startsWith("/")) { 314 // @yannis : temporary way to avoid DocumentNotFoundException in server log before NXP-19658 315 Documents documents = CLIENT.repository().query(String.format(DOCUMENT_QUERY_BY_PATH_BASE, idOrPath)); 316 if (documents.size() > 0) { 317 CLIENT.repository().deleteDocument(documents.getDocument(0)); 318 } 319 } else { 320 CLIENT.repository().deleteDocument(idOrPath); 321 } 322 } 323 324 public static void addPermission(String idOrPath, String username, String permission) { 325 ACE ace = new ACE(); 326 ace.setUsername(username); 327 ace.setPermission(permission); 328 329 fetchDocumentByIdOrPath(idOrPath).addPermission(ace); 330 } 331 332 public static void removePermissions(String idOrPath, String username) { 333 fetchDocumentByIdOrPath(idOrPath).removePermission(username); 334 } 335 336 /** 337 * @since 9.3 338 */ 339 public static void followLifecycleTransition(String idOrPath, String transitionName) { 340 CLIENT.operation("Document.FollowLifecycleTransition") 341 .input(new DocRef(idOrPath)) 342 .param("value", transitionName) 343 .execute(); 344 } 345 346 /** 347 * @since 9.3 348 */ 349 public static boolean documentExists(String idOrPath) { 350 return exists(() -> fetchDocumentByIdOrPath(idOrPath)); 351 } 352 353 /** 354 * @since 9.3 355 */ 356 public static void startWorkflowInstance(String idOrPath, String workflowId) { 357 Workflow workflow = CLIENT.repository().fetchWorkflowModel(workflowId); 358 if (idOrPath.startsWith("/")) { 359 CLIENT.repository().startWorkflowInstanceWithDocPath(idOrPath, workflow); 360 } else { 361 CLIENT.repository().startWorkflowInstanceWithDocId(idOrPath, workflow); 362 } 363 } 364 365 /** 366 * @since 9.3 367 */ 368 public static boolean documentHasWorkflowStarted(String idOrPath) { 369 Workflows workflows; 370 if (idOrPath.startsWith("/")) { 371 workflows = CLIENT.repository().fetchWorkflowInstancesByDocPath(idOrPath); 372 } else { 373 workflows = CLIENT.repository().fetchWorkflowInstancesByDocId(idOrPath); 374 } 375 return workflows.size() > 0; 376 } 377 378 /** 379 * @since 10.10 380 */ 381 public static String getWorkflowInstanceTitle(String workflowId) { 382 Workflow workflow = CLIENT.repository().fetchWorkflowModel(workflowId); 383 return workflow.getTitle(); 384 } 385 386 /** 387 * Fetches a {@link Document} instance according the input parameter which can be a document id or path. 388 * <p /> 389 * CAUTION: Keep this method protected, we want to keep nuxeo-java-client objects here. 390 * 391 * @since 9.3 392 */ 393 protected static Document fetchDocumentByIdOrPath(String idOrPath) { 394 if (idOrPath.startsWith("/")) { 395 return CLIENT.repository().fetchDocumentByPath(idOrPath); 396 } else { 397 return CLIENT.repository().fetchDocumentById(idOrPath); 398 } 399 } 400 401 /** 402 * Runs a page provider on Nuxeo instance and return the total size of documents. 403 * 404 * @return the total size of documents 405 * @since 9.3 406 */ 407 public static int countQueryPageProvider(String providerName) { 408 Documents result = CLIENT.repository().queryByProvider(providerName, "1", "0", "-1", "dc:title", "ASC", null); 409 return result.getTotalSize(); 410 } 411 412 // ------------------ 413 // Directory Services 414 // ------------------ 415 416 /** 417 * @since 9.2 418 */ 419 public static String createDirectoryEntry(String directoryName, Map<String, String> properties) { 420 DirectoryEntry entry = new DirectoryEntry(); 421 entry.setProperties(properties); 422 entry = CLIENT.directoryManager().directory(directoryName).createEntry(entry); 423 String entryId = entry.getId(); 424 addDirectoryEntryToDelete(directoryName, entryId); 425 return entryId; 426 } 427 428 /** 429 * @since 9.3 430 */ 431 public static Map<String, Object> fetchDirectoryEntryProperties(String directoryName, String entryId) { 432 return CLIENT.directoryManager().directory(directoryName).fetchEntry(entryId).getProperties(); 433 } 434 435 /** 436 * @since 9.10 437 */ 438 public static void updateDirectoryEntry(String directoryName, String entryId, Map<String, String> properties) { 439 DirectoryEntry entry = new DirectoryEntry(); 440 entry.setProperties(properties); 441 entry.putIdProperty(entryId); 442 CLIENT.directoryManager().directory(directoryName).updateEntry(entry); 443 } 444 445 /** 446 * @since 9.2 447 */ 448 public static void deleteDirectoryEntry(String directoryName, String entryId) { 449 CLIENT.directoryManager().directory(directoryName).deleteEntry(entryId); 450 } 451 452 /** 453 * @since 9.10 454 */ 455 public static void deleteDirectoryEntries(String directoryName) { 456 CLIENT.directoryManager().directory(directoryName).fetchEntries().getDirectoryEntries().forEach(entry -> { 457 entry.delete(); 458 removeDirectoryEntryToDelete(directoryName, entry.getId()); 459 }); 460 } 461 462 // ------------------ 463 // Operation Services 464 // ------------------ 465 466 /** 467 * @since 9.3 468 */ 469 public static void operation(String operationId, Map<String, Object> parameters) { 470 CLIENT.operation(operationId).parameters(parameters).execute(); 471 } 472 473 /** 474 * Logs on server with <code>RestHelper</code> as source and <code>warn</code> as level. 475 * 476 * @since 9.3 477 */ 478 public static void logOnServer(String message) { 479 logOnServer("warn", message); 480 } 481 482 /** 483 * Logs on server with <code>RestHelper</code> as source. 484 */ 485 public static void logOnServer(String level, String message) { 486 logOnServer("RestHelper", level, message); 487 } 488 489 /** 490 * @param source the logger source, usually RestHelper or WebDriver 491 * @param level the log level 492 * @since 9.3 493 */ 494 public static void logOnServer(String source, String level, String message) { 495 CLIENT.operation("Log") 496 // For compatibility 497 .param("category", RestHelper.class.getName()) 498 .param("level", level) 499 .param("message", String.format("----- %s: %s", source, message)) 500 .execute(); 501 } 502 503 // ------------- 504 // HTTP Services 505 // ------------- 506 507 /** 508 * Performs a GET request and return whether or not request was successful. 509 * 510 * @since 9.3 511 */ 512 public static boolean get(String path) { 513 return executeHTTP(() -> CLIENT.get(NUXEO_URL + path)); 514 } 515 516 /** 517 * Performs a POST request and return whether or not request was successful. 518 * 519 * @since 9.3 520 */ 521 public static boolean post(String path, String body) { 522 return executeHTTP(() -> CLIENT.post(NUXEO_URL + path, body)); 523 } 524 525 /** 526 * @since 9.3 527 */ 528 protected static boolean executeHTTP(Supplier<Response> fetcher) { 529 Response response = fetcher.get(); 530 response.body().close(); 531 return response.isSuccessful(); 532 } 533 534 /** 535 * @since 9.3 536 */ 537 protected static <T> boolean exists(Supplier<T> fetcher) { 538 try { 539 return fetcher.get() != null; 540 } catch (NuxeoClientRemoteException nce) { 541 if (nce.getStatus() == HttpStatus.SC_NOT_FOUND) { 542 return false; 543 } 544 throw nce; 545 } 546 } 547 548}