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