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 7.10
147     */
148    @Param(name = "quotePatternParameters", required = false, description = "Quote query parameters if the query is specified")
149    protected boolean quotePatternParameters = true;
150
151    /**
152     * @since 7.10
153     */
154    @Param(name = "escapePatternParameters", required = false, description = "Escape query parameters if the query is specified")
155    protected boolean escapePatternParameters = true;
156
157    /**
158     * @since 6.0
159     */
160    @Param(name = "sortOrder", required = false, description = "Sort order, " + "ASC or DESC", widget = Constants.W_OPTION, values = {
161            ASC, DESC })
162    protected String sortOrder;
163
164    @SuppressWarnings("unchecked")
165    @OperationMethod
166    public PaginableDocumentModelListImpl run() throws OperationException {
167        List<SortInfo> sortInfos = null;
168        if (sortInfoAsStringList != null) {
169            // BBB
170            sortInfos = new ArrayList<SortInfo>();
171            for (String sortInfoDesc : sortInfoAsStringList) {
172                SortInfo sortInfo;
173                if (sortInfoDesc.contains(SORT_PARAMETER_SEPARATOR)) {
174                    String[] parts = sortInfoDesc.split(SORT_PARAMETER_SEPARATOR);
175                    sortInfo = new SortInfo(parts[0], Boolean.parseBoolean(parts[1]));
176                } else {
177                    sortInfo = new SortInfo(sortInfoDesc, true);
178                }
179                sortInfos.add(sortInfo);
180            }
181        } else {
182            // Sort Info Management
183            if (!StringUtils.isBlank(sortBy)) {
184                sortInfos = new ArrayList<>();
185                String[] sorts = sortBy.split(",");
186                String[] orders = null;
187                if (!StringUtils.isBlank(sortOrder)) {
188                    orders = sortOrder.split(",");
189                }
190                for (int i = 0; i < sorts.length; i++) {
191                    String sort = sorts[i];
192                    boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase()));
193                    sortInfos.add(new SortInfo(sort, sortAscending));
194                }
195            }
196        }
197
198        Object[] parameters = null;
199
200        if (strParameters != null && !strParameters.isEmpty()) {
201            parameters = strParameters.toArray(new String[strParameters.size()]);
202            // expand specific parameters
203            for (int idx = 0; idx < parameters.length; idx++) {
204                String value = (String) parameters[idx];
205                if (value.equals(CURRENT_USERID_PATTERN)) {
206                    parameters[idx] = session.getPrincipal().getName();
207                } else if (value.equals(CURRENT_REPO_PATTERN)) {
208                    parameters[idx] = session.getRepositoryName();
209                }
210            }
211        }
212
213        Map<String, Serializable> props = new HashMap<String, Serializable>();
214        props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) session);
215
216        if (query == null && StringUtils.isBlank(providerName)) {
217            // provide a defaut query
218            query = "SELECT * from Document";
219        }
220
221        Long targetPage = null;
222        if (page != null) {
223            targetPage = page.longValue();
224        }
225        if (currentPageIndex != null) {
226            targetPage = currentPageIndex.longValue();
227        }
228        Long targetPageSize = null;
229        if (pageSize != null) {
230            targetPageSize = pageSize.longValue();
231        }
232
233        DocumentModel searchDocumentModel = getSearchDocumentModel(session, ppService, providerName, namedParameters);
234
235        PaginableDocumentModelListImpl res;
236        if (query != null) {
237            CoreQueryPageProviderDescriptor desc = new CoreQueryPageProviderDescriptor();
238            desc.setPattern(query);
239            desc.setQuotePatternParameters(quotePatternParameters);
240            desc.setEscapePatternParameters(escapePatternParameters);
241            if (maxResults != null && !maxResults.isEmpty() && !maxResults.equals("-1")) {
242                // set the maxResults to avoid slowing down queries
243                desc.getProperties().put("maxResults", maxResults);
244            }
245            PageProvider<DocumentModel> pp = (PageProvider<DocumentModel>) ppService.getPageProvider("", desc,
246                    searchDocumentModel, sortInfos, targetPageSize, targetPage, props, parameters);
247            res = new PaginableDocumentModelListImpl(pp, documentLinkBuilder);
248        } else {
249            PageProvider<DocumentModel> pp = (PageProvider<DocumentModel>) ppService.getPageProvider(providerName,
250                    searchDocumentModel, sortInfos, targetPageSize, targetPage, props,
251                    context.containsKey("seamActionContext") ? getParameters(providerName, parameters) : parameters);
252            res = new PaginableDocumentModelListImpl(pp, documentLinkBuilder);
253        }
254        if (res.hasError()) {
255            throw new OperationException(res.getErrorMessage());
256        }
257        return res;
258    }
259
260    /**
261     * Resolves additional parameters that could have been defined in the contribution.
262     *
263     * @param pageProviderName name of the Page Provider
264     * @param givenParameters parameters from the operation
265     * @since 5.8
266     */
267    private Object[] getParameters(final String pageProviderName, final Object[] givenParameters) {
268        // resolve additional parameters
269        PageProviderDefinition ppDef = ppService.getPageProviderDefinition(pageProviderName);
270        String[] params = ppDef.getQueryParameters();
271        if (params == null) {
272            params = new String[0];
273        }
274
275        Object[] resolvedParams = new Object[params.length + (givenParameters != null ? givenParameters.length : 0)];
276
277        ELContext elContext = EL.createELContext(SeamActionContext.EL_RESOLVER, new FunctionMapperImpl());
278
279        int i = 0;
280        if (givenParameters != null) {
281            i = givenParameters.length;
282            System.arraycopy(givenParameters, 0, resolvedParams, 0, i);
283        }
284        for (int j = 0; j < params.length; j++) {
285            ValueExpression ve = SeamActionContext.EXPRESSION_FACTORY.createValueExpression(elContext, params[j],
286                    Object.class);
287            resolvedParams[i + j] = ve.getValue(elContext);
288        }
289        return resolvedParams;
290    }
291
292    /**
293     * @since 7.1
294     */
295    public static DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps,
296            String providerName, Properties namedParameters) {
297        // generate search document model if type specified on the definition
298        DocumentModel searchDocumentModel = null;
299        if (!StringUtils.isBlank(providerName)) {
300            PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName);
301            if (pageProviderDefinition != null) {
302                String searchDocType = pageProviderDefinition.getSearchDocumentType();
303                if (searchDocType != null) {
304                    searchDocumentModel = session.createDocumentModel(searchDocType);
305                } else if (pageProviderDefinition.getWhereClause() != null) {
306                    // avoid later error on null search doc, in case where clause is only referring to named parameters
307                    // (and no namedParameters are given)
308                    searchDocumentModel = new SimpleDocumentModel();
309                }
310            } else {
311                log.error("No page provider definition found for " + providerName);
312            }
313        }
314
315        if (namedParameters != null && !namedParameters.isEmpty()) {
316            // fall back on simple document if no type defined on page provider
317            if (searchDocumentModel == null) {
318                searchDocumentModel = new SimpleDocumentModel();
319            }
320            for (Map.Entry<String, String> entry : namedParameters.entrySet()) {
321                String key = entry.getKey();
322                String value = entry.getValue();
323                try {
324                    DocumentHelper.setProperty(session, searchDocumentModel, key, value, true);
325                } catch (PropertyNotFoundException | IOException e) {
326                    // assume this is a "pure" named parameter, not part of the search doc schema
327                    continue;
328                }
329            }
330            searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters);
331        }
332        return searchDocumentModel;
333    }
334
335}