001/*
002 * (C) Copyright 2006-2012 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 *
018 * $Id$
019 */
020package org.nuxeo.ecm.platform.routing.web;
021
022import java.io.Serializable;
023import java.sql.Timestamp;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.List;
030import java.util.Map;
031
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.SortInfo;
035import org.nuxeo.ecm.platform.audit.api.AuditReader;
036import org.nuxeo.ecm.platform.audit.api.LogEntry;
037import org.nuxeo.ecm.platform.audit.api.comment.CommentProcessorHelper;
038import org.nuxeo.ecm.platform.query.api.AbstractPageProvider;
039import org.nuxeo.ecm.platform.query.api.PageProvider;
040import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
041import org.nuxeo.ecm.platform.query.api.PredicateDefinition;
042import org.nuxeo.ecm.platform.query.api.PredicateFieldDefinition;
043import org.nuxeo.runtime.api.Framework;
044
045/**
046 * {@link PageProvider} implementation that returns {@link LogEntry} from Audit Service - Used for Route History content
047 * view
048 *
049 * @Since 5.6
050 */
051public class PreviousRoutesPageProvider extends AbstractPageProvider<LogEntry> implements PageProvider<LogEntry> {
052
053    private static final long serialVersionUID = 1L;
054
055    protected String auditQuery;
056
057    protected Map<String, Object> auditQueryParams;
058
059    public static final String CORE_SESSION_PROPERTY = "coreSession";
060
061    public static final String UICOMMENTS_PROPERTY = "generateUIComments";
062
063    public static final String DOC_ID_PROPERTY = "documentID";
064
065    @Override
066    public String toString() {
067        StringBuffer sb = new StringBuffer();
068
069        sb.append("\nquery : " + auditQuery);
070        sb.append("\nparams : ");
071        List<String> pNames = new ArrayList<String>(auditQueryParams.keySet());
072        Collections.sort(pNames);
073        for (String name : pNames) {
074            sb.append("\n ");
075            sb.append(name);
076            sb.append(" : ");
077            sb.append(auditQueryParams.get(name).toString());
078        }
079
080        return sb.toString();
081    }
082
083    protected void preprocessCommentsIfNeeded(List<LogEntry> entries) {
084        Serializable preprocess = getProperties().get(UICOMMENTS_PROPERTY);
085        CoreSession session = (CoreSession) getProperties().get(CORE_SESSION_PROPERTY);
086        if (session != null && preprocess != null && "true".equalsIgnoreCase(preprocess.toString())) {
087            CommentProcessorHelper cph = new CommentProcessorHelper(session);
088            cph.processComments(entries);
089        }
090    }
091
092    @SuppressWarnings("unchecked")
093    @Override
094    public List<LogEntry> getCurrentPage() {
095        AuditReader reader = Framework.getService(AuditReader.class);
096
097        buildAuditQuery(true);
098        // TODO: Follow https://jira.nuxeo.com/browse/NXP-17236, native query should use JSON in case of Elasticsearch
099        // audit backend
100        List<LogEntry> entries = (List<LogEntry>) reader.nativeQuery(auditQuery, auditQueryParams,
101                (int) getCurrentPageIndex() + 1, (int) getMinMaxPageSize());
102        preprocessCommentsIfNeeded(entries);
103        return entries;
104    }
105
106    protected String getSortPart() {
107        StringBuffer sort = new StringBuffer();
108        if (getSortInfos() != null && getSortInfos().size() > 0) {
109            sort.append(" ORDER BY ");
110        }
111        int index = 0;
112        for (SortInfo si : getSortInfos()) {
113            if (index > 0) {
114                sort.append(" , ");
115            }
116            sort.append(si.getSortColumn());
117            if (si.getSortAscending()) {
118                sort.append(" ASC ");
119            } else {
120                sort.append(" DESC ");
121            }
122            index++;
123        }
124        return sort.toString();
125    }
126
127    protected Object convertParam(Object param) {
128        if (param == null) {
129            return null;
130        }
131        // Hibernate does not like Calendar type
132        if (param instanceof Calendar) {
133            return new Timestamp(((Calendar) param).getTime().getTime());
134        }
135        return param;
136    }
137
138    protected boolean isNonNullParam(Object[] val) {
139        if (val == null) {
140            return false;
141        }
142        for (Object v : val) {
143            if (v != null) {
144                if (v instanceof String) {
145                    if (!((String) v).isEmpty()) {
146                        return true;
147                    }
148                } else if (v instanceof String[]) {
149                    if (((String[]) v).length > 0) {
150                        return true;
151                    }
152                } else {
153                    return true;
154                }
155            }
156        }
157        return false;
158    }
159
160    protected String getFixedPart() {
161        if (getDefinition().getWhereClause() == null) {
162            return null;
163        } else {
164            return getDefinition().getWhereClause().getFixedPart();
165        }
166    }
167
168    protected boolean allowSimplePattern() {
169        return true;
170    }
171
172    protected void buildAuditQuery(boolean includeSort) {
173        PageProviderDefinition def = getDefinition();
174        Object[] params = getParameters();
175
176        if (def.getWhereClause() == null) {
177            // Simple Pattern
178
179            if (!allowSimplePattern()) {
180                throw new UnsupportedOperationException("This page provider requires a explicit Where Clause");
181            }
182
183            String baseQuery = def.getPattern();
184
185            Map<String, Object> qParams = new HashMap<String, Object>();
186            for (int i = 0; i < params.length; i++) {
187                baseQuery = baseQuery.replaceFirst("\\?", ":param" + i);
188                qParams.put("param" + i, convertParam(params[i]));
189            }
190
191            if (includeSort) {
192                baseQuery = baseQuery + getSortPart();
193            }
194
195            auditQuery = baseQuery;
196            auditQueryParams = qParams;
197
198        } else {
199            // Where clause based on DocumentModel
200
201            StringBuilder baseQuery = new StringBuilder("from LogEntry log ");
202
203            // manage fixed part
204            String fixedPart = getFixedPart();
205            Map<String, Object> qParams = new HashMap<String, Object>();
206            int idxParam = 0;
207            if (fixedPart != null && !fixedPart.isEmpty()) {
208                while (fixedPart.indexOf("?") > 0) {
209                    // Hack for handling parameter in fixed part TODO
210                    if (getProperties().get(DOC_ID_PROPERTY) != null) {
211                        fixedPart = fixedPart.replaceFirst("\\?", getProperties().get(DOC_ID_PROPERTY).toString());
212                        // Map properties = new HashMap<String, Serializable>();
213                        // properties.put(DOC_ID_PROPERTY, "done");
214                    } else {
215                        fixedPart = fixedPart.replaceFirst("\\?", ":param" + idxParam);
216                        qParams.put("param" + idxParam, convertParam(params[idxParam]));
217                        idxParam++;
218                    }
219                }
220                baseQuery.append(" where ");
221                baseQuery.append(fixedPart);
222            }
223
224            // manages predicates
225            DocumentModel searchDocumentModel = getSearchDocumentModel();
226            if (searchDocumentModel != null) {
227                PredicateDefinition[] predicates = def.getWhereClause().getPredicates();
228                int idxPredicate = 0;
229
230                for (PredicateDefinition predicate : predicates) {
231
232                    // extract data from DocumentModel
233                    PredicateFieldDefinition[] fieldDef = predicate.getValues();
234                    Object[] val = new Object[fieldDef.length];
235                    for (int fidx = 0; fidx < fieldDef.length; fidx++) {
236                        if (fieldDef[fidx].getXpath() != null) {
237                            val[fidx] = searchDocumentModel.getPropertyValue(fieldDef[fidx].getXpath());
238                        } else {
239                            val[fidx] = searchDocumentModel.getProperty(fieldDef[fidx].getSchema(),
240                                    fieldDef[fidx].getName());
241                        }
242                    }
243
244                    if (!isNonNullParam(val)) {
245                        // skip predicate where all values are null
246                        continue;
247                    }
248
249                    if (idxPredicate > 0 || idxParam > 0) {
250                        baseQuery.append(" AND ");
251                    } else {
252                        baseQuery.append(" where ");
253                    }
254
255                    baseQuery.append(predicate.getParameter());
256                    baseQuery.append(" ");
257
258                    if (!predicate.getOperator().equalsIgnoreCase("BETWEEN")) {
259                        // don't add the between operation for now
260                        baseQuery.append(predicate.getOperator());
261                    }
262
263                    if (predicate.getOperator().equalsIgnoreCase("IN")) {
264                        baseQuery.append(" (");
265
266                        if (val[0] instanceof Iterable<?>) {
267                            Iterable<?> vals = (Iterable<?>) val[0];
268                            Iterator<?> valueIterator = vals.iterator();
269
270                            while (valueIterator.hasNext()) {
271                                Object v = valueIterator.next();
272                                qParams.put("param" + idxParam, convertParam(v));
273                                baseQuery.append(" :param" + idxParam);
274                                idxParam++;
275                                if (valueIterator.hasNext()) {
276                                    baseQuery.append(",");
277                                }
278                            }
279                        } else if (val[0] instanceof Object[]) {
280                            Object[] valArray = (Object[]) val[0];
281                            for (int i = 0; i < valArray.length; i++) {
282                                Object v = valArray[i];
283                                qParams.put("param" + idxParam, convertParam(v));
284                                baseQuery.append(" :param" + idxParam);
285                                idxParam++;
286                                if (i < valArray.length - 1) {
287                                    baseQuery.append(",");
288                                }
289                            }
290                        }
291                        baseQuery.append(" ) ");
292                    } else if (predicate.getOperator().equalsIgnoreCase("BETWEEN")) {
293                        Object startValue = convertParam(val[0]);
294                        Object endValue = null;
295                        if (val.length > 1) {
296                            endValue = convertParam(val[1]);
297                        }
298                        if (startValue != null && endValue != null) {
299                            baseQuery.append(predicate.getOperator());
300                            baseQuery.append(" :param" + idxParam);
301                            qParams.put("param" + idxParam, startValue);
302                            idxParam++;
303                            baseQuery.append(" AND :param" + idxParam);
304                            qParams.put("param" + idxParam, endValue);
305                        } else if (startValue == null) {
306                            baseQuery.append("<=");
307                            baseQuery.append(" :param" + idxParam);
308                            qParams.put("param" + idxParam, endValue);
309                        } else if (endValue == null) {
310                            baseQuery.append(">=");
311                            baseQuery.append(" :param" + idxParam);
312                            qParams.put("param" + idxParam, startValue);
313                        }
314                        idxParam++;
315                    } else {
316                        baseQuery.append(" :param" + idxParam);
317                        qParams.put("param" + idxParam, convertParam(val[0]));
318                        idxParam++;
319                    }
320
321                    idxPredicate++;
322                }
323            }
324
325            if (includeSort) {
326                baseQuery.append(getSortPart());
327            }
328
329            auditQuery = baseQuery.toString();
330            auditQueryParams = qParams;
331        }
332    }
333
334    @Override
335    public void refresh() {
336        setCurrentPageOffset(0);
337        super.refresh();
338    }
339
340    @SuppressWarnings("unchecked")
341    @Override
342    public long getResultsCount() {
343        buildAuditQuery(false);
344        AuditReader reader = Framework.getService(AuditReader.class);
345        // TODO: Follow https://jira.nuxeo.com/browse/NXP-17236, native query should use JSON in case of Elasticsearch
346        // audit backend, yet currently there doesn't seem to be any way to do such a count in this case
347        List<Long> res = (List<Long>) reader.nativeQuery("select count(log.id) " + auditQuery, auditQueryParams, 1, 20);
348        resultsCount = res.get(0).longValue();
349        return resultsCount;
350    }
351
352}