001/* 002 * (C) Copyright 2016-2017 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 <kleturc@nuxeo.com> 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.lang.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 = "devnull@nuxeo.com"; 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 * Fetches a {@link Document} instance according the input parameter which can be a document id or path. 380 * <p /> 381 * CAUTION: Keep this method protected, we want to keep nuxeo-java-client objects here. 382 * 383 * @since 9.3 384 */ 385 protected static Document fetchDocumentByIdOrPath(String idOrPath) { 386 if (idOrPath.startsWith("/")) { 387 return CLIENT.repository().fetchDocumentByPath(idOrPath); 388 } else { 389 return CLIENT.repository().fetchDocumentById(idOrPath); 390 } 391 } 392 393 /** 394 * Runs a page provider on Nuxeo instance and return the total size of documents. 395 * 396 * @return the total size of documents 397 * @since 9.3 398 */ 399 public static int countQueryPageProvider(String providerName) { 400 Documents result = CLIENT.repository().queryByProvider(providerName, "1", "0", "-1", "dc:title", "ASC", null); 401 return result.getTotalSize(); 402 } 403 404 // ------------------ 405 // Directory Services 406 // ------------------ 407 408 /** 409 * @since 9.2 410 */ 411 public static String createDirectoryEntry(String directoryName, Map<String, String> properties) { 412 DirectoryEntry entry = new DirectoryEntry(); 413 entry.setProperties(properties); 414 entry = CLIENT.directoryManager().directory(directoryName).createEntry(entry); 415 String entryId = entry.getId(); 416 addDirectoryEntryToDelete(directoryName, entryId); 417 return entryId; 418 } 419 420 /** 421 * @since 9.3 422 */ 423 public static Map<String, Object> fetchDirectoryEntryProperties(String directoryName, String entryId) { 424 return CLIENT.directoryManager().directory(directoryName).fetchEntry(entryId).getProperties(); 425 } 426 427 /** 428 * @since 9.10 429 */ 430 public static void updateDirectoryEntry(String directoryName, String entryId, Map<String, String> properties) { 431 DirectoryEntry entry = new DirectoryEntry(); 432 entry.setProperties(properties); 433 entry.putIdProperty(entryId); 434 CLIENT.directoryManager().directory(directoryName).updateEntry(entry); 435 } 436 437 /** 438 * @since 9.2 439 */ 440 public static void deleteDirectoryEntry(String directoryName, String entryId) { 441 CLIENT.directoryManager().directory(directoryName).deleteEntry(entryId); 442 } 443 444 /** 445 * @since 9.10 446 */ 447 public static void deleteDirectoryEntries(String directoryName) { 448 CLIENT.directoryManager().directory(directoryName).fetchEntries().getDirectoryEntries().forEach(entry -> { 449 entry.delete(); 450 removeDirectoryEntryToDelete(directoryName, entry.getId()); 451 }); 452 } 453 454 // ------------------ 455 // Operation Services 456 // ------------------ 457 458 /** 459 * @since 9.3 460 */ 461 public static void operation(String operationId, Map<String, Object> parameters) { 462 CLIENT.operation(operationId).parameters(parameters).execute(); 463 } 464 465 /** 466 * Logs on server with <code>RestHelper</code> as source and <code>warn</code> as level. 467 * 468 * @since 9.3 469 */ 470 public static void logOnServer(String message) { 471 logOnServer("warn", message); 472 } 473 474 /** 475 * Logs on server with <code>RestHelper</code> as source. 476 */ 477 public static void logOnServer(String level, String message) { 478 logOnServer("RestHelper", level, message); 479 } 480 481 /** 482 * @param source the logger source, usually RestHelper or WebDriver 483 * @param level the log level 484 * @since 9.3 485 */ 486 public static void logOnServer(String source, String level, String message) { 487 CLIENT.operation("Log") 488 // For compatibility 489 .param("category", RestHelper.class.getName()) 490 .param("level", level) 491 .param("message", String.format("----- %s: %s", source, message)) 492 .execute(); 493 } 494 495 // ------------- 496 // HTTP Services 497 // ------------- 498 499 /** 500 * Performs a GET request and return whether or not request was successful. 501 * 502 * @since 9.3 503 */ 504 public static boolean get(String path) { 505 return executeHTTP(() -> CLIENT.get(NUXEO_URL + path)); 506 } 507 508 /** 509 * Performs a POST request and return whether or not request was successful. 510 * 511 * @since 9.3 512 */ 513 public static boolean post(String path, String body) { 514 return executeHTTP(() -> CLIENT.post(NUXEO_URL + path, body)); 515 } 516 517 /** 518 * @since 9.3 519 */ 520 protected static boolean executeHTTP(Supplier<Response> fetcher) { 521 Response response = fetcher.get(); 522 response.body().close(); 523 return response.isSuccessful(); 524 } 525 526 /** 527 * @since 9.3 528 */ 529 protected static <T> boolean exists(Supplier<T> fetcher) { 530 try { 531 return fetcher.get() != null; 532 } catch (NuxeoClientRemoteException nce) { 533 if (nce.getStatus() == HttpStatus.SC_NOT_FOUND) { 534 return false; 535 } 536 throw nce; 537 } 538 } 539 540}