001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (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 *     Thierry Delprat
018 *     Marwane Kalam-Alami
019 */
020package org.nuxeo.ecm.automation.core.operations.services;
021
022import java.io.IOException;
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.el.ELContext;
031import javax.el.ValueExpression;
032
033import org.apache.commons.lang.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.automation.OperationContext;
037import org.nuxeo.ecm.automation.OperationException;
038import org.nuxeo.ecm.automation.core.Constants;
039import org.nuxeo.ecm.automation.core.annotations.Context;
040import org.nuxeo.ecm.automation.core.annotations.Operation;
041import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
042import org.nuxeo.ecm.automation.core.annotations.Param;
043import org.nuxeo.ecm.automation.core.util.DocumentHelper;
044import org.nuxeo.ecm.automation.core.util.Properties;
045import org.nuxeo.ecm.automation.core.util.StringList;
046import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl;
047import org.nuxeo.ecm.core.api.CoreSession;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.api.SortInfo;
050import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
051import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
052import org.nuxeo.ecm.core.query.sql.NXQL;
053import org.nuxeo.ecm.platform.actions.ActionContext;
054import org.nuxeo.ecm.platform.actions.ELActionContext;
055import org.nuxeo.ecm.platform.el.ELService;
056import org.nuxeo.ecm.platform.query.api.PageProvider;
057import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
058import org.nuxeo.ecm.platform.query.api.PageProviderService;
059import org.nuxeo.ecm.platform.query.api.QuickFilter;
060import org.nuxeo.ecm.platform.query.core.CoreQueryPageProviderDescriptor;
061import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider;
062import org.nuxeo.runtime.api.Framework;
063
064/**
065 * Operation to execute a query or a named provider with support for Pagination.
066 *
067 * @author Tiry (tdelprat@nuxeo.com)
068 * @since 5.4.2
069 */
070@Operation(id = DocumentPageProviderOperation.ID, category = Constants.CAT_FETCH, label = "PageProvider", description = "Perform "
071        + "a query or a named provider query on the repository. Result is "
072        + "paginated. The query result will become the input for the next "
073        + "operation. If no query or provider name is given, a query returning "
074        + "all the documents that the user has access to will be executed.", aliases = { "Document.PageProvider" })
075public class DocumentPageProviderOperation {
076
077    public static final String ID = "Repository.PageProvider";
078
079    public static final String CURRENT_USERID_PATTERN = "$currentUser";
080
081    public static final String CURRENT_REPO_PATTERN = "$currentRepository";
082
083    private static final String SORT_PARAMETER_SEPARATOR = " ";
084
085    public static final String ASC = "ASC";
086
087    public static final String DESC = "DESC";
088
089    private static final Log log = LogFactory.getLog(DocumentPageProviderOperation.class);
090
091    @Context
092    protected OperationContext context;
093
094    @Context
095    protected CoreSession session;
096
097    @Context
098    protected PageProviderService ppService;
099
100    @Param(name = "providerName", required = false)
101    protected String providerName;
102
103    /**
104     * @deprecated since 6.0 use instead {@link org.nuxeo.ecm.automation .core.operations.services.query.DocumentQuery}.
105     */
106    @Deprecated
107    @Param(name = "query", required = false)
108    protected String query;
109
110    @Param(name = "language", required = false, widget = Constants.W_OPTION, values = { NXQL.NXQL })
111    protected String lang = NXQL.NXQL;
112
113    /** @deprecated since 6.0 use currentPageIndex instead. */
114    @Param(name = "page", required = false)
115    @Deprecated
116    protected Integer page;
117
118    @Param(name = "currentPageIndex", required = false)
119    protected Integer currentPageIndex;
120
121    @Param(name = "pageSize", required = false)
122    protected Integer pageSize;
123
124    /**
125     * @deprecated since 6.0 use instead {@link #sortBy and @link #sortOrder}.
126     */
127    @Deprecated
128    @Param(name = "sortInfo", required = false)
129    protected StringList sortInfoAsStringList;
130
131    @Param(name = "queryParams", alias = "searchTerm", required = false)
132    protected StringList strParameters;
133
134    @Param(name = "documentLinkBuilder", required = false)
135    protected String documentLinkBuilder;
136
137    /**
138     * @since 5.7
139     */
140    @Param(name = "maxResults", required = false)
141    protected String maxResults = "100";
142
143    /**
144     * @since 6.0
145     */
146    @Param(name = PageProviderService.NAMED_PARAMETERS, required = false, description = "Named parameters to pass to the page provider to "
147            + "fill in query variables.")
148    protected Properties namedParameters;
149
150    /**
151     * @since 6.0
152     */
153    @Param(name = "sortBy", required = false, description = "Sort by " + "properties (separated by comma)")
154    protected String sortBy;
155
156    /**
157     * @since 7.10
158     */
159    @Param(name = "quotePatternParameters", required = false, description = "Quote query parameters if the query is specified")
160    protected boolean quotePatternParameters = true;
161
162    /**
163     * @since 7.10
164     */
165    @Param(name = "escapePatternParameters", required = false, description = "Escape query parameters if the query is specified")
166    protected boolean escapePatternParameters = true;
167
168    /**
169     * @since 6.0
170     */
171    @Param(name = "sortOrder", required = false, description = "Sort order, "
172            + "ASC or DESC", widget = Constants.W_OPTION, values = { ASC, DESC })
173    protected String sortOrder;
174
175    /**
176     * @since 8.4
177     */
178    @Param(name = "quickFilters", required = false, description = "Quick filter " + "properties (separated by comma)")
179    protected String quickFilters;
180
181    /**
182     * @since 9.1
183     */
184    @Param(name = "highlights", required = false, description = "Highlight properties (separated by comma)")
185    protected String highlight;
186
187    @SuppressWarnings("unchecked")
188    @OperationMethod
189    public PaginableDocumentModelListImpl run() throws OperationException {
190        List<SortInfo> sortInfos = null;
191        if (sortInfoAsStringList != null) {
192            // BBB
193            sortInfos = new ArrayList<SortInfo>();
194            for (String sortInfoDesc : sortInfoAsStringList) {
195                SortInfo sortInfo;
196                if (sortInfoDesc.contains(SORT_PARAMETER_SEPARATOR)) {
197                    String[] parts = sortInfoDesc.split(SORT_PARAMETER_SEPARATOR);
198                    sortInfo = new SortInfo(parts[0], Boolean.parseBoolean(parts[1]));
199                } else {
200                    sortInfo = new SortInfo(sortInfoDesc, true);
201                }
202                sortInfos.add(sortInfo);
203            }
204        } else {
205            // Sort Info Management
206            if (!StringUtils.isBlank(sortBy)) {
207                sortInfos = new ArrayList<>();
208                String[] sorts = sortBy.split(",");
209                String[] orders = null;
210                if (!StringUtils.isBlank(sortOrder)) {
211                    orders = sortOrder.split(",");
212                }
213                for (int i = 0; i < sorts.length; i++) {
214                    String sort = sorts[i];
215                    boolean sortAscending = (orders != null && orders.length > i
216                            && "asc".equals(orders[i].toLowerCase()));
217                    sortInfos.add(new SortInfo(sort, sortAscending));
218                }
219            }
220        }
221
222        Object[] parameters = null;
223
224        if (strParameters != null && !strParameters.isEmpty()) {
225            parameters = strParameters.toArray(new String[strParameters.size()]);
226            // expand specific parameters
227            for (int idx = 0; idx < parameters.length; idx++) {
228                String value = (String) parameters[idx];
229                if (value.equals(CURRENT_USERID_PATTERN)) {
230                    parameters[idx] = session.getPrincipal().getName();
231                } else if (value.equals(CURRENT_REPO_PATTERN)) {
232                    parameters[idx] = session.getRepositoryName();
233                }
234            }
235        }
236
237        Map<String, Serializable> props = new HashMap<String, Serializable>();
238        props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) session);
239
240        if (query == null && StringUtils.isBlank(providerName)) {
241            // provide a defaut query
242            query = "SELECT * from Document";
243        }
244
245        Long targetPage = null;
246        if (page != null) {
247            targetPage = page.longValue();
248        }
249        if (currentPageIndex != null) {
250            targetPage = currentPageIndex.longValue();
251        }
252        Long targetPageSize = null;
253        if (pageSize != null) {
254            targetPageSize = pageSize.longValue();
255        }
256
257        List<String> targetHighlights = null;
258        if (highlight != null) {
259            String[] highlights = highlight.split(",");
260            targetHighlights = Arrays.asList(highlights);
261        }
262
263        DocumentModel searchDocumentModel = getSearchDocumentModel(session, ppService, providerName, namedParameters);
264
265        PaginableDocumentModelListImpl res;
266        if (query != null) {
267            CoreQueryPageProviderDescriptor desc = new CoreQueryPageProviderDescriptor();
268            desc.setPattern(query);
269            desc.setQuotePatternParameters(quotePatternParameters);
270            desc.setEscapePatternParameters(escapePatternParameters);
271            if (maxResults != null && !maxResults.isEmpty() && !maxResults.equals("-1")) {
272                // set the maxResults to avoid slowing down queries
273                desc.getProperties().put("maxResults", maxResults);
274            }
275            PageProvider<DocumentModel> pp = (PageProvider<DocumentModel>) ppService.getPageProvider("", desc,
276                    searchDocumentModel, sortInfos, targetPageSize, targetPage, props, targetHighlights, null,
277                    parameters);
278            res = new PaginableDocumentModelListImpl(pp, documentLinkBuilder);
279        } else {
280            PageProviderDefinition pageProviderDefinition = ppService.getPageProviderDefinition(providerName);
281            // Quick filters management
282            List<QuickFilter> quickFilterList = new ArrayList<>();
283            if (quickFilters != null && !quickFilters.isEmpty()) {
284                String[] filters = quickFilters.split(",");
285                List<QuickFilter> ppQuickFilters = pageProviderDefinition.getQuickFilters();
286                for (String filter : filters) {
287                    for (QuickFilter quickFilter : ppQuickFilters) {
288                        if (quickFilter.getName().equals(filter)) {
289                            quickFilterList.add(quickFilter);
290                            break;
291                        }
292                    }
293                }
294            }
295
296            parameters = resolveParameters(parameters);
297            PageProvider<DocumentModel> pp = (PageProvider<DocumentModel>) ppService.getPageProvider(providerName,
298                    searchDocumentModel, sortInfos, targetPageSize, targetPage, props, targetHighlights,
299                    quickFilterList, parameters);
300            res = new PaginableDocumentModelListImpl(pp, documentLinkBuilder);
301        }
302        if (res.hasError()) {
303            throw new OperationException(res.getErrorMessage());
304        }
305        return res;
306    }
307
308    /**
309     * Resolves additional parameters that could have been defined in the contribution.
310     *
311     * @param parameters parameters from the operation
312     * @since 5.8
313     */
314    private Object[] resolveParameters(Object[] parameters) {
315        ActionContext actionContext = (ActionContext) context.get(GetActions.SEAM_ACTION_CONTEXT);
316        if (actionContext == null) {
317            // if no Seam Context has been initialized, don't do evaluation
318            return parameters;
319        }
320
321        // resolve additional parameters
322        PageProviderDefinition ppDef = ppService.getPageProviderDefinition(providerName);
323        String[] params = ppDef.getQueryParameters();
324        if (params == null) {
325            params = new String[0];
326        }
327
328        Object[] resolvedParams = new Object[params.length + (parameters != null ? parameters.length : 0)];
329
330        ELContext elContext = Framework.getService(ELService.class).createELContext();
331
332        int i = 0;
333        if (parameters != null) {
334            i = parameters.length;
335            System.arraycopy(parameters, 0, resolvedParams, 0, i);
336        }
337        for (int j = 0; j < params.length; j++) {
338            ValueExpression ve = ELActionContext.EXPRESSION_FACTORY.createValueExpression(elContext, params[j],
339                    Object.class);
340            resolvedParams[i + j] = ve.getValue(elContext);
341        }
342        return resolvedParams;
343    }
344
345    /**
346     * @since 7.1
347     */
348    public static DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps,
349            String providerName, Properties namedParameters) {
350        // generate search document model if type specified on the definition
351        DocumentModel searchDocumentModel = null;
352        if (!StringUtils.isBlank(providerName)) {
353            PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName);
354            if (pageProviderDefinition != null) {
355                String searchDocType = pageProviderDefinition.getSearchDocumentType();
356                if (searchDocType != null) {
357                    searchDocumentModel = session.createDocumentModel(searchDocType);
358                } else if (pageProviderDefinition.getWhereClause() != null) {
359                    // avoid later error on null search doc, in case where clause is only referring to named parameters
360                    // (and no namedParameters are given)
361                    searchDocumentModel = new SimpleDocumentModel();
362                }
363            } else {
364                log.error("No page provider definition found for " + providerName);
365            }
366        }
367
368        if (namedParameters != null && !namedParameters.isEmpty()) {
369            // fall back on simple document if no type defined on page provider
370            if (searchDocumentModel == null) {
371                searchDocumentModel = new SimpleDocumentModel();
372            }
373            for (Map.Entry<String, String> entry : namedParameters.entrySet()) {
374                String key = entry.getKey();
375                String value = entry.getValue();
376                try {
377                    DocumentHelper.setProperty(session, searchDocumentModel, key, value, true);
378                } catch (PropertyNotFoundException | IOException e) {
379                    // assume this is a "pure" named parameter, not part of the search doc schema
380                    continue;
381                }
382            }
383            searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters);
384        }
385        return searchDocumentModel;
386    }
387
388}