001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Thierry Delprat
011 *     Marwane Kalam-Alami
012 */
013package org.nuxeo.ecm.automation.core.operations.services;
014
015import java.io.IOException;
016import java.io.Serializable;
017import java.util.ArrayList;
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021
022import javax.el.ELContext;
023import javax.el.ValueExpression;
024
025import org.apache.commons.lang.StringUtils;
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.jboss.el.lang.FunctionMapperImpl;
029import org.jboss.seam.el.EL;
030import org.nuxeo.ecm.automation.OperationContext;
031import org.nuxeo.ecm.automation.OperationException;
032import org.nuxeo.ecm.automation.core.Constants;
033import org.nuxeo.ecm.automation.core.annotations.Context;
034import org.nuxeo.ecm.automation.core.annotations.Operation;
035import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
036import org.nuxeo.ecm.automation.core.annotations.Param;
037import org.nuxeo.ecm.automation.core.util.DocumentHelper;
038import org.nuxeo.ecm.automation.core.util.Properties;
039import org.nuxeo.ecm.automation.core.util.StringList;
040import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.SortInfo;
044import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
045import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
046import org.nuxeo.ecm.core.query.sql.NXQL;
047import org.nuxeo.ecm.platform.actions.seam.SeamActionContext;
048import org.nuxeo.ecm.platform.query.api.PageProvider;
049import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
050import org.nuxeo.ecm.platform.query.api.PageProviderService;
051import org.nuxeo.ecm.platform.query.core.CoreQueryPageProviderDescriptor;
052import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider;
053
054/**
055 * Operation to execute a query or a named provider with support for Pagination.
056 *
057 * @author Tiry (tdelprat@nuxeo.com)
058 * @since 5.4.2
059 */
060@Operation(id = DocumentPageProviderOperation.ID, category = Constants.CAT_FETCH, label = "PageProvider", description = "Perform "
061        + "a query or a named provider query on the repository. Result is "
062        + "paginated. The query result will become the input for the next "
063        + "operation. If no query or provider name is given, a query returning "
064        + "all the documents that the user has access to will be executed.", aliases = { "Document.PageProvider" })
065public class DocumentPageProviderOperation {
066
067    public static final String ID = "Repository.PageProvider";
068
069    public static final String CURRENT_USERID_PATTERN = "$currentUser";
070
071    public static final String CURRENT_REPO_PATTERN = "$currentRepository";
072
073    private static final String SORT_PARAMETER_SEPARATOR = " ";
074
075    public static final String ASC = "ASC";
076
077    public static final String DESC = "DESC";
078
079    private static final Log log = LogFactory.getLog(DocumentPageProviderOperation.class);
080
081    @Context
082    protected OperationContext context;
083
084    @Context
085    protected CoreSession session;
086
087    @Context
088    protected PageProviderService ppService;
089
090    @Param(name = "providerName", required = false)
091    protected String providerName;
092
093    /**
094     * @deprecated since 6.0 use instead {@link org.nuxeo.ecm.automation .core.operations.services.query.DocumentQuery}.
095     */
096    @Deprecated
097    @Param(name = "query", required = false)
098    protected String query;
099
100    @Param(name = "language", required = false, widget = Constants.W_OPTION, values = { NXQL.NXQL })
101    protected String lang = NXQL.NXQL;
102
103    @Param(name = "page", required = false)
104    @Deprecated
105    protected Integer page;
106
107    @Param(name = "currentPageIndex", required = false)
108    protected Integer currentPageIndex;
109
110    @Param(name = "pageSize", required = false)
111    protected Integer pageSize;
112
113    /**
114     * @deprecated since 6.0 use instead {@link #sortBy and @link #sortOrder}.
115     */
116    @Deprecated
117    @Param(name = "sortInfo", required = false)
118    protected StringList sortInfoAsStringList;
119
120    @Param(name = "queryParams", alias = "searchTerm", required = false)
121    protected StringList strParameters;
122
123    @Param(name = "documentLinkBuilder", required = false)
124    protected String documentLinkBuilder;
125
126    /**
127     * @since 5.7
128     */
129    @Param(name = "maxResults", required = false)
130    protected String maxResults = "100";
131
132    /**
133     * @since 6.0
134     */
135    @Param(name = PageProviderService.NAMED_PARAMETERS, required = false, description = "Named parameters to pass to the page provider to "
136            + "fill in query variables.")
137    protected Properties namedParameters;
138
139    /**
140     * @since 6.0
141     */
142    @Param(name = "sortBy", required = false, description = "Sort by " + "properties (separated by comma)")
143    protected String sortBy;
144
145    /**
146     * @since 6.0
147     */
148    @Param(name = "sortOrder", required = false, description = "Sort order, " + "ASC or DESC", widget = Constants.W_OPTION, values = {
149            ASC, DESC })
150    protected String sortOrder;
151
152    @SuppressWarnings("unchecked")
153    @OperationMethod
154    public PaginableDocumentModelListImpl run() throws OperationException {
155        List<SortInfo> sortInfos = null;
156        if (sortInfoAsStringList != null) {
157            // BBB
158            sortInfos = new ArrayList<SortInfo>();
159            for (String sortInfoDesc : sortInfoAsStringList) {
160                SortInfo sortInfo;
161                if (sortInfoDesc.contains(SORT_PARAMETER_SEPARATOR)) {
162                    String[] parts = sortInfoDesc.split(SORT_PARAMETER_SEPARATOR);
163                    sortInfo = new SortInfo(parts[0], Boolean.parseBoolean(parts[1]));
164                } else {
165                    sortInfo = new SortInfo(sortInfoDesc, true);
166                }
167                sortInfos.add(sortInfo);
168            }
169        } else {
170            // Sort Info Management
171            if (!StringUtils.isBlank(sortBy)) {
172                sortInfos = new ArrayList<>();
173                String[] sorts = sortBy.split(",");
174                String[] orders = null;
175                if (!StringUtils.isBlank(sortOrder)) {
176                    orders = sortOrder.split(",");
177                }
178                for (int i = 0; i < sorts.length; i++) {
179                    String sort = sorts[i];
180                    boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase()));
181                    sortInfos.add(new SortInfo(sort, sortAscending));
182                }
183            }
184        }
185
186        Object[] parameters = null;
187
188        if (strParameters != null && !strParameters.isEmpty()) {
189            parameters = strParameters.toArray(new String[strParameters.size()]);
190            // expand specific parameters
191            for (int idx = 0; idx < parameters.length; idx++) {
192                String value = (String) parameters[idx];
193                if (value.equals(CURRENT_USERID_PATTERN)) {
194                    parameters[idx] = session.getPrincipal().getName();
195                } else if (value.equals(CURRENT_REPO_PATTERN)) {
196                    parameters[idx] = session.getRepositoryName();
197                }
198            }
199        }
200
201        Map<String, Serializable> props = new HashMap<String, Serializable>();
202        props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) session);
203
204        if (query == null && StringUtils.isBlank(providerName)) {
205            // provide a defaut query
206            query = "SELECT * from Document";
207        }
208
209        Long targetPage = null;
210        if (page != null) {
211            targetPage = page.longValue();
212        }
213        if (currentPageIndex != null) {
214            targetPage = currentPageIndex.longValue();
215        }
216        Long targetPageSize = null;
217        if (pageSize != null) {
218            targetPageSize = pageSize.longValue();
219        }
220
221        DocumentModel searchDocumentModel = getSearchDocumentModel(session, ppService, providerName, namedParameters);
222
223        PaginableDocumentModelListImpl res;
224        if (query != null) {
225            CoreQueryPageProviderDescriptor desc = new CoreQueryPageProviderDescriptor();
226            desc.setPattern(query);
227            if (maxResults != null && !maxResults.isEmpty() && !maxResults.equals("-1")) {
228                // set the maxResults to avoid slowing down queries
229                desc.getProperties().put("maxResults", maxResults);
230            }
231            PageProvider<DocumentModel> pp = (PageProvider<DocumentModel>) ppService.getPageProvider("", desc,
232                    searchDocumentModel, sortInfos, targetPageSize, targetPage, props, parameters);
233            res = new PaginableDocumentModelListImpl(pp, documentLinkBuilder);
234        } else {
235            PageProvider<DocumentModel> pp = (PageProvider<DocumentModel>) ppService.getPageProvider(providerName,
236                    searchDocumentModel, sortInfos, targetPageSize, targetPage, props,
237                    context.containsKey("seamActionContext") ? getParameters(providerName, parameters) : parameters);
238            res = new PaginableDocumentModelListImpl(pp, documentLinkBuilder);
239        }
240        if (res.hasError()) {
241            throw new OperationException(res.getErrorMessage());
242        }
243        return res;
244    }
245
246    /**
247     * Resolves additional parameters that could have been defined in the contribution.
248     *
249     * @param pageProviderName name of the Page Provider
250     * @param givenParameters parameters from the operation
251     * @since 5.8
252     */
253    private Object[] getParameters(final String pageProviderName, final Object[] givenParameters) {
254        // resolve additional parameters
255        PageProviderDefinition ppDef = ppService.getPageProviderDefinition(pageProviderName);
256        String[] params = ppDef.getQueryParameters();
257        if (params == null) {
258            params = new String[0];
259        }
260
261        Object[] resolvedParams = new Object[params.length + (givenParameters != null ? givenParameters.length : 0)];
262
263        ELContext elContext = EL.createELContext(SeamActionContext.EL_RESOLVER, new FunctionMapperImpl());
264
265        int i = 0;
266        if (givenParameters != null) {
267            i = givenParameters.length;
268            System.arraycopy(givenParameters, 0, resolvedParams, 0, i);
269        }
270        for (int j = 0; j < params.length; j++) {
271            ValueExpression ve = SeamActionContext.EXPRESSION_FACTORY.createValueExpression(elContext, params[j],
272                    Object.class);
273            resolvedParams[i + j] = ve.getValue(elContext);
274        }
275        return resolvedParams;
276    }
277
278    /**
279     * @since 7.1
280     */
281    public static DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps,
282            String providerName, Properties namedParameters) {
283        // generate search document model if type specified on the definition
284        DocumentModel searchDocumentModel = null;
285        if (!StringUtils.isBlank(providerName)) {
286            PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName);
287            if (pageProviderDefinition != null) {
288                String searchDocType = pageProviderDefinition.getSearchDocumentType();
289                if (searchDocType != null) {
290                    searchDocumentModel = session.createDocumentModel(searchDocType);
291                } else if (pageProviderDefinition.getWhereClause() != null) {
292                    // avoid later error on null search doc, in case where clause is only referring to named parameters
293                    // (and no namedParameters are given)
294                    searchDocumentModel = new SimpleDocumentModel();
295                }
296            } else {
297                log.error("No page provider definition found for " + providerName);
298            }
299        }
300
301        if (namedParameters != null && !namedParameters.isEmpty()) {
302            // fall back on simple document if no type defined on page provider
303            if (searchDocumentModel == null) {
304                searchDocumentModel = new SimpleDocumentModel();
305            }
306            for (Map.Entry<String, String> entry : namedParameters.entrySet()) {
307                String key = entry.getKey();
308                String value = entry.getValue();
309                try {
310                    DocumentHelper.setProperty(session, searchDocumentModel, key, value, true);
311                } catch (PropertyNotFoundException | IOException e) {
312                    // assume this is a "pure" named parameter, not part of the search doc schema
313                    continue;
314                }
315            }
316            searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters);
317        }
318        return searchDocumentModel;
319    }
320
321}