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 <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.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 = "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     * @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}