001/*
002 * (C) Copyright 2010 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 *     Anahide Tchertchian
018 */
019package org.nuxeo.ecm.platform.smart.query.jsf;
020
021import java.io.Serializable;
022import java.util.List;
023
024import javax.faces.application.FacesMessage;
025import javax.faces.component.EditableValueHolder;
026import javax.faces.component.UIComponent;
027import javax.faces.context.FacesContext;
028import javax.faces.event.ActionEvent;
029import javax.faces.event.AjaxBehaviorEvent;
030import javax.faces.validator.ValidatorException;
031
032import org.apache.commons.lang.StringUtils;
033import org.jboss.seam.ScopeType;
034import org.jboss.seam.annotations.Name;
035import org.jboss.seam.annotations.Observer;
036import org.jboss.seam.annotations.Scope;
037import org.jboss.seam.annotations.intercept.BypassInterceptors;
038import org.jboss.seam.annotations.web.RequestParameter;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.core.api.SortInfo;
041import org.nuxeo.ecm.platform.smart.query.HistoryList;
042import org.nuxeo.ecm.platform.smart.query.SmartQuery;
043import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
044import org.nuxeo.runtime.logging.DeprecationLogger;
045import org.nuxeo.search.ui.seam.SearchUIActions;
046
047/**
048 * Seam component handling a {@link IncrementalSmartNXQLQuery} instance created for a given existing query string.
049 * <p>
050 * Also handles undo/redo actions and all ajax interactions for an incremental query build page.
051 *
052 * @since 5.4
053 * @author Anahide Tchertchian
054 */
055@Name("smartNXQLQueryActions")
056@Scope(ScopeType.PAGE)
057public class SmartNXQLQueryActions implements Serializable {
058
059    private static final long serialVersionUID = 1L;
060
061    public static final int HISTORY_CAPACITY = 20;
062
063    /**
064     * @deprecated since 8.1: query part held/set directly on the content view search document model
065     */
066    protected String queryPart;
067
068    protected HistoryList<String> queryPartHistory;
069
070    protected HistoryList<String> redoQueryPartHistory;
071
072    protected IncrementalSmartNXQLQuery currentSmartQuery;
073
074    /**
075     * @deprecated since 8.1: selected columns directly held/set on the content view search document model
076     */
077    @Deprecated
078    protected List<String> selectedLayoutColumns;
079
080    /**
081     * @deprecated since 8.1: search sort infos directly held/set on the content view search document model
082     */
083    @Deprecated
084    protected List<SortInfo> searchSortInfos;
085
086    /**
087     * Request parameter passed from the widget that makes it possible to decide whether the backing object should be
088     * updated when performing ajax calls.
089     * <p>
090     * For instance, on the smart search form, any ajax action should update the backing property {@link #queryPart}.
091     * When the query part is held by a document property, it should not be updated on ajax actions: only the global
092     * submit of the form should impact it.
093     *
094     * @deprecated since 8.1: query part is now held by the content view search document model, consider it does not
095     *             need to be updated until user clicks on "filter".
096     */
097    @RequestParameter
098    @Deprecated
099    protected Boolean updateQueryPart;
100
101    /**
102     * Request parameter making it possible to find the component holding the query part to update it.
103     */
104    @RequestParameter
105    protected String queryPartComponentId;
106
107    /**
108     * @deprecated since 8.1: query part held/set directly on the content view search document model
109     */
110    @Deprecated
111    public String getQueryPart() {
112        DeprecationLogger.log("Query part held/set directly on the content view search document model", "8.1");
113        return queryPart;
114    }
115
116    /**
117     * @deprecated since 8.1: query part held/set directly on the content view search document model
118     */
119    public void setQueryPart(String queryPart) {
120        DeprecationLogger.log("Query part held/set directly on the content view search document model", "8.1");
121        this.queryPart = queryPart;
122        addToQueryPartHistory(queryPart);
123    }
124
125    /**
126     * @deprecated since 8.1: selected columns directly held/set on the content view search document model
127     */
128    @Deprecated
129    public List<String> getSelectedLayoutColumns() {
130        DeprecationLogger.log("Selected columns held/set directly on the content view search document model", "8.1");
131        return selectedLayoutColumns;
132    }
133
134    /**
135     * @deprecated since 8.1: selected columns directly held/set on the content view search document model
136     */
137    @Deprecated
138    public void setSelectedLayoutColumns(List<String> selectedLayoutColumns) {
139        DeprecationLogger.log("Selected columns held/set directly on the content view search document model", "8.1");
140        this.selectedLayoutColumns = selectedLayoutColumns;
141    }
142
143    /**
144     * @deprecated since 8.1: selected columns directly held/set on the content view search document model
145     */
146    @Deprecated
147    public void resetSelectedLayoutColumns() {
148        DeprecationLogger.log("Selected columns held/set directly on the content view search document model", "8.1");
149        setSelectedLayoutColumns(null);
150    }
151
152    /**
153     * @deprecated since 8.1: search sort infos directly held/set on the content view search document model
154     */
155    @Deprecated
156    public List<SortInfo> getSearchSortInfos() {
157        DeprecationLogger.log("Search sort infos held/set directly on the content view search document model", "8.1");
158        return searchSortInfos;
159    }
160
161    /**
162     * @deprecated since 8.1: search sort infos directly held/set on the content view search document model
163     */
164    @Deprecated
165    public void setSearchSortInfos(List<SortInfo> searchSortInfos) {
166        DeprecationLogger.log("Search sort infos held/set directly on the content view search document model", "8.1");
167        this.searchSortInfos = searchSortInfos;
168    }
169
170    public void initCurrentSmartQuery(String existingQueryPart, boolean resetHistory) {
171        currentSmartQuery = new IncrementalSmartNXQLQuery(existingQueryPart);
172        if (resetHistory) {
173            queryPartHistory = null;
174            addToQueryPartHistory(existingQueryPart);
175        }
176    }
177
178    /**
179     * Creates a new {@link #currentSmartQuery} instance.
180     * <p>
181     * This method is supposed to be called once when loading a new page: it will initialize the smart query object
182     * according to the current existing qury part.
183     * <p>
184     * It should not be called when there are validation errors happening on the page, otherwise the new query part may
185     * be discarded.
186     */
187    public void initCurrentSmartQuery(String existingQueryPart) {
188        initCurrentSmartQuery(existingQueryPart, true);
189    }
190
191    public SmartQuery getCurrentSmartQuery() {
192        if (currentSmartQuery == null) {
193            initCurrentSmartQuery("", true);
194        }
195        return currentSmartQuery;
196    }
197
198    /**
199     * Updates the current {@link #currentSmartQuery} instance according to changes on the existing query part.
200     */
201    public void queryPartChanged(AjaxBehaviorEvent event) {
202        UIComponent comp = event.getComponent();
203        if (comp instanceof EditableValueHolder) {
204            EditableValueHolder queryComp = (EditableValueHolder) comp;
205            String newQuery = (String) queryComp.getSubmittedValue();
206            // set local value in case of validation error in ajax region
207            // when adding a new item to the query
208            queryComp.setValue(newQuery);
209            // update query
210            currentSmartQuery.setExistingQueryPart(newQuery);
211            addToQueryPartHistory(newQuery);
212        } else {
213            throw new NuxeoException("Component not found");
214        }
215    }
216
217    /**
218     * Updates the JSF component holding the query part.
219     *
220     * @param event the JSF event that will give an anchor on the JSF tree to find the target component.
221     * @param newQuery the new query to set.
222     * @param rebuildSmartQuery if true, will rebuild the smart query completely, otherwise will just set the query part
223     *            on it.
224     * @throws NuxeoException if target JSF component is not found in the JSF tree.
225     */
226    protected void setQueryPart(ActionEvent event, String newQuery, boolean rebuildSmartQuery) {
227        if (currentSmartQuery != null) {
228            UIComponent component = event.getComponent();
229            if (component == null) {
230                return;
231            }
232            // find component to update in the hierarchy of JSF components:
233            // this is specific to rendering structure...
234            EditableValueHolder queryPartComp = ComponentUtils.getComponent(component, queryPartComponentId,
235                    EditableValueHolder.class);
236            if (queryPartComp != null) {
237                // set submitted value to ensure validation
238                queryPartComp.setSubmittedValue(newQuery);
239                // set local value in case of validation error in ajax region
240                // when adding a new item to the query
241                queryPartComp.setValue(newQuery);
242                if (rebuildSmartQuery) {
243                    // rebuild smart query
244                    initCurrentSmartQuery(newQuery, false);
245                } else {
246                    currentSmartQuery.setExistingQueryPart(newQuery);
247                }
248                addToQueryPartHistory(newQuery);
249            } else {
250                throw new NuxeoException("Component not found");
251            }
252        }
253    }
254
255    /**
256     * Updates the query part, asking the {@link #currentSmartQuery} to build the new resulting query.
257     *
258     * @see #setQueryPart(ActionEvent, String)
259     */
260    public void buildQueryPart(ActionEvent event) {
261        if (currentSmartQuery != null) {
262            String newQuery = currentSmartQuery.buildQuery();
263            setQueryPart(event, newQuery, true);
264        }
265    }
266
267    /**
268     * Sets the query part to an empty value.
269     *
270     * @see #setQueryPart(ActionEvent, String)
271     */
272    public void clearQueryPart(ActionEvent event) {
273        setQueryPart(event, "", false);
274    }
275
276    protected String getCurrentQueryPart() {
277        if (currentSmartQuery != null) {
278            return currentSmartQuery.getExistingQueryPart();
279        }
280        return null;
281    }
282
283    protected boolean hasQueryPartHistory(HistoryList<String> history) {
284        if (history == null || history.isEmpty()) {
285            return false;
286        }
287        String lastQueryPart = history.getLast();
288        // lastQueryPart cannot be null
289        if (history.size() == 1 && lastQueryPart.equals(getCurrentQueryPart())) {
290            return false;
291        }
292        return true;
293    }
294
295    public boolean getCanUndoQueryPartChanges() {
296        return hasQueryPartHistory(queryPartHistory);
297    }
298
299    public boolean getCanRedoQueryPartChanges() {
300        return hasQueryPartHistory(redoQueryPartHistory);
301    }
302
303    public void undoHistoryChanges(ActionEvent event, HistoryList<String> history, HistoryList<String> redoHistory) {
304        if (!hasQueryPartHistory(history)) {
305            return;
306        }
307        String lastQueryPart = history.getLast();
308        history.removeLast();
309        String currentQueryPart = getCurrentQueryPart();
310        // lastQueryPart cannot be null
311        if (!lastQueryPart.equals(currentQueryPart)) {
312            setQueryPart(event, lastQueryPart, false);
313        } else if (history.size() > 0) {
314            lastQueryPart = history.getLast();
315            setQueryPart(event, lastQueryPart, false);
316            history.removeLast();
317        }
318        if (redoHistory != null) {
319            addToHistory(currentQueryPart, redoHistory);
320        }
321    }
322
323    public void undoQueryPartChanges(ActionEvent event) {
324        if (redoQueryPartHistory == null) {
325            redoQueryPartHistory = new HistoryList<String>(HISTORY_CAPACITY);
326        }
327        undoHistoryChanges(event, queryPartHistory, redoQueryPartHistory);
328    }
329
330    public void redoQueryPartChanges(ActionEvent event) {
331        undoHistoryChanges(event, redoQueryPartHistory, null);
332    }
333
334    protected void addToHistory(String queryPart, HistoryList<String> queryPartHistory) {
335        if (queryPartHistory == null) {
336            return;
337        }
338        if (queryPart == null) {
339            queryPart = "";
340        }
341        if (queryPartHistory.size() == 0) {
342            queryPartHistory.addLast(queryPart);
343        } else {
344            String lastQueryPart = queryPartHistory.getLast();
345            if (!queryPart.equals(lastQueryPart)) {
346                queryPartHistory.addLast(queryPart);
347            }
348        }
349    }
350
351    protected void addToQueryPartHistory(String queryPart) {
352        if (queryPartHistory == null) {
353            queryPartHistory = new HistoryList<String>(HISTORY_CAPACITY);
354        }
355        addToHistory(queryPart, queryPartHistory);
356    }
357
358    /**
359     * Validates the query part: throws a {@link ValidatorException} if query is not a String, or if
360     * {@link IncrementalSmartNXQLQuery#isValid(String)} returns false.
361     *
362     * @see IncrementalSmartNXQLQuery#isValid(String)
363     */
364    public void validateQueryPart(FacesContext context, UIComponent component, Object value) {
365        if (value == null) {
366            return;
367        }
368        if (!(value instanceof String)) {
369            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
370                    ComponentUtils.translate(context, "error.smart.query.invalidQuery"), null);
371            // also add global message
372            context.addMessage(null, message);
373            throw new ValidatorException(message);
374        }
375        String query = (String) value;
376        if (StringUtils.isBlank(query)) {
377            return;
378        }
379        if (!IncrementalSmartNXQLQuery.isValid(query)) {
380            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
381                    ComponentUtils.translate(context, "error.smart.query.invalidQuery"), null);
382            // also add global message
383            context.addMessage(null, message);
384            throw new ValidatorException(message);
385        }
386    }
387
388    /**
389     * Returns true if current request is an ajax request.
390     * <p>
391     * Useful when some component should be required only when the global form is submitted, and not when ajax calls are
392     * performed.
393     */
394    public boolean isAjaxRequest() {
395        DeprecationLogger.log(
396                "smartNXQLQueryActions#isAjaxRequest is not needed anymore, proper ajax calls make it possible to validate or not a field depending on use cases.",
397                "8.1");
398        FacesContext context = FacesContext.getCurrentInstance();
399        if (context != null) {
400            return context.getPartialViewContext().isAjaxRequest();
401        }
402        return false;
403    }
404
405    /**
406     * Returns a valid where clause from a query part.
407     * <p>
408     * Useful to avoid generating an invalid query if query part is empty (especially if content view is not marked as
409     * waiting for first execution).
410     *
411     * @since 8.1
412     */
413    public String getWhereClause(String queryPart, boolean followedByClause) {
414        if (StringUtils.isBlank(queryPart)) {
415            if (followedByClause) {
416                return "WHERE ";
417            }
418            return "";
419        }
420        return "WHERE (" + queryPart + ")" + (followedByClause ? " AND " : "");
421    }
422
423    /**
424     * @since 8.1
425     */
426    public boolean isInitialized() {
427        return currentSmartQuery != null;
428    }
429
430    /**
431     * @since 8.1
432     */
433    @Observer(value = { SearchUIActions.SEARCH_SELECTED_EVENT }, create = false)
434    @BypassInterceptors
435    public void resetCurrentSmartQuery() {
436        currentSmartQuery = null;
437    }
438
439}