View Javadoc
1   /**
2    * Copyright 2014-2024 Bloomreach B.V. (<a href="http://www.bloomreach.com">http://www.bloomreach.com</a>)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         <a href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.onehippo.forge.exdocpicker.impl.field.tree;
17  
18  import java.io.Serializable;
19  import java.util.ArrayList;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.Set;
23  
24  import org.apache.commons.lang3.StringUtils;
25  import org.apache.wicket.AttributeModifier;
26  import org.apache.wicket.Component;
27  import org.apache.wicket.ajax.AjaxRequestTarget;
28  import org.apache.wicket.ajax.markup.html.form.AjaxButton;
29  import org.apache.wicket.behavior.Behavior;
30  import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
31  import org.apache.wicket.extensions.markup.html.repeater.data.table.NoRecordsToolbar;
32  import org.apache.wicket.extensions.markup.html.repeater.tree.AbstractTree;
33  import org.apache.wicket.extensions.markup.html.repeater.tree.TableTree;
34  import org.apache.wicket.extensions.markup.html.repeater.tree.content.CheckedFolder;
35  import org.apache.wicket.extensions.markup.html.repeater.tree.table.TreeColumn;
36  import org.apache.wicket.extensions.markup.html.repeater.tree.theme.HumanTheme;
37  import org.apache.wicket.extensions.markup.html.repeater.tree.theme.WindowsTheme;
38  import org.apache.wicket.markup.ComponentTag;
39  import org.apache.wicket.markup.head.CssHeaderItem;
40  import org.apache.wicket.markup.head.IHeaderResponse;
41  import org.apache.wicket.markup.repeater.Item;
42  import org.apache.wicket.markup.repeater.OddEvenItem;
43  import org.apache.wicket.model.IModel;
44  import org.apache.wicket.model.Model;
45  import org.apache.wicket.request.resource.PackageResourceReference;
46  import org.onehippo.forge.exdocpicker.api.ExternalDocumentCollection;
47  import org.onehippo.forge.exdocpicker.api.ExternalDocumentServiceContext;
48  import org.onehippo.forge.exdocpicker.api.ExternalDocumentServiceFacade;
49  import org.onehippo.forge.exdocpicker.api.PluginConstants;
50  import org.onehippo.forge.exdocpicker.impl.field.AbstractExternalDocumentFieldBrowserDialog;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  /**
55   * External document field picker dialog implementation with the default tree list view UI, providing a basic tree
56   * navigation capability and expand-all / collapse-all buttons.
57   */
58  public class ExternalTreeItemFieldBrowserDialog extends AbstractExternalDocumentFieldBrowserDialog {
59  
60      private static final long serialVersionUID = 1L;
61  
62      private static final Logger log = LoggerFactory.getLogger(ExternalTreeItemFieldBrowserDialog.class);
63  
64      /**
65       * {@link ExternalTreeItemDataProvider} instance.
66       */
67      private ExternalTreeItemDataProvider<Serializable> treeDataProvider;
68  
69      /**
70       * {@link TreeItemExpansionSet} instance.
71       */
72      private TreeItemExpansionSet treeExpansionSet;
73  
74      /**
75       * Tree list view UI theme.
76       */
77      private final Behavior theme;
78  
79      /**
80       * Constructs external document(s) picker dialog.
81       * @param titleModel title model
82       * @param extDocServiceContext {@link ExternalDocumentServiceContext} instance
83       * @param exdocService {@link ExternalDocumentServiceFacade} instance
84       * @param model the model containing the currently selected external documents in the document node data
85       */
86      public ExternalTreeItemFieldBrowserDialog(IModel<String> titleModel,
87              final ExternalDocumentServiceContext extDocServiceContext,
88              final ExternalDocumentServiceFacade<Serializable> exdocService,
89              IModel<ExternalDocumentCollection<Serializable>> model) {
90          super(titleModel, extDocServiceContext, exdocService, model);
91  
92          final String themeName = extDocServiceContext.getPluginConfig().getString(
93                  PluginConstants.PARAM_EXTERNAL_TREE_VIEW_THEME, PluginConstants.DEFAULT_EXTERNAL_TREE_VIEW_THEME);
94  
95          if (StringUtils.startsWithIgnoreCase(themeName, "window")) {
96              theme = new WindowsTheme();
97          } else {
98              theme = new HumanTheme();
99          }
100 
101         AjaxButton expandSelectedButton = new AjaxButton("expand-selected-button") {
102             @Override
103             protected void onSubmit(AjaxRequestTarget target) {
104                 expandPickedExternalTreeItemNodes(getExternalDocumentServiceFacade());
105                 target.add(ExternalTreeItemFieldBrowserDialog.this);
106             }
107         };
108         add(expandSelectedButton);
109 
110         AjaxButton expandAllButton = new AjaxButton("expand-all-button") {
111             @Override
112             protected void onSubmit(AjaxRequestTarget target) {
113                 treeExpansionSet.expandAll();
114                 target.add(ExternalTreeItemFieldBrowserDialog.this);
115             }
116         };
117         add(expandAllButton);
118 
119         AjaxButton collapseAllButton = new AjaxButton("collapse-all-button") {
120             @Override
121             protected void onSubmit(AjaxRequestTarget target) {
122                 treeExpansionSet.collapseAll();
123                 target.add(ExternalTreeItemFieldBrowserDialog.this);
124             }
125         };
126         add(collapseAllButton);
127     }
128 
129     /**
130      * {@inheritDoc}
131      */
132     @Override
133     public void renderHead(IHeaderResponse response) {
134         super.renderHead(response);
135         response.render(
136                 CssHeaderItem.forReference(new PackageResourceReference(ExternalTreeItemFieldBrowserDialog.class,
137                         ExternalTreeItemFieldBrowserDialog.class.getSimpleName() + ".css")));
138     }
139 
140     /**
141      * {@inheritDoc}
142      */
143     @Override
144     protected void initializeSearchedExternalDocuments() {
145         searchExternalTreeNodeItems();
146     }
147 
148     /**
149      * {@inheritDoc}
150      */
151     @SuppressWarnings({ "unchecked", "rawtypes" })
152     @Override
153     protected void initializeDataListView() {
154         treeExpansionSet = new TreeItemExpansionSet();
155 
156         final int initialExpandDepth = getExternalDocumentServiceContext().getPluginConfig()
157                 .getAsInteger(PluginConstants.PARAM_INITIAL_TREE_EXPAND_DEPTH, 0);
158 
159         // Expand tree nodes to the initial depth level if configured.
160         if (initialExpandDepth > 0) {
161             for (Iterator<? extends Serializable> itemIt = getSearchedExternalDocuments().iterator(); itemIt
162                     .hasNext();) {
163                 expandExternalTreeItemNode(getExternalDocumentServiceFacade(), itemIt.next(), 0, initialExpandDepth);
164             }
165         }
166 
167         // Expand parent tree nodes of currently selected items.
168         expandPickedExternalTreeItemNodes(getExternalDocumentServiceFacade());
169 
170         treeDataProvider = new ExternalTreeItemDataProvider<>(getSearchedExternalDocuments(),
171                 getExternalDocumentServiceFacade());
172         AbstractTree<Serializable> treeDataView = createTree(new Model(treeExpansionSet));
173         treeDataView.setOutputMarkupId(true);
174 
175         treeDataView.add(new Behavior() {
176             private static final long serialVersionUID = 1L;
177 
178             @Override
179             public void onComponentTag(Component component, ComponentTag tag) {
180                 theme.onComponentTag(component, tag);
181             }
182 
183             @Override
184             public void renderHead(Component component, IHeaderResponse response) {
185                 theme.renderHead(component, response);
186             }
187         });
188 
189         add(treeDataView);
190     }
191 
192     /**
193      * Search tree node items.
194      */
195     protected void searchExternalTreeNodeItems() {
196         try {
197             ExternalDocumentCollection<? extends Serializable> searchedDocs = getExternalDocumentServiceFacade()
198                     .searchExternalDocuments(getExternalDocumentServiceContext(), "");
199 
200             getSearchedExternalDocuments().clear();
201 
202             if (searchedDocs != null && searchedDocs.getSize() > 0) {
203                 for (Iterator<? extends Serializable> it = searchedDocs.iterator(); it.hasNext();) {
204                     getSearchedExternalDocuments().add(it.next());
205                 }
206             }
207         } catch (Exception e) {
208             log.error("Failed to execute search external tree items.", e);
209         }
210     }
211 
212     /**
213      * Create a tree list view UI component.
214      * @param state expansion state
215      * @return a tree list view UI component
216      */
217     protected AbstractTree<Serializable> createTree(IModel<Set<Serializable>> state) {
218         List<IColumn<Serializable, String>> columns = createColumns();
219 
220         final TableTree<Serializable, String> tree = new TableTree<Serializable, String>("tree", columns,
221                 treeDataProvider, Integer.MAX_VALUE, state) {
222             private static final long serialVersionUID = 1L;
223 
224             @Override
225             protected Component newContentComponent(String id, IModel<Serializable> model) {
226                 CheckableTreeNode treeNode = new CheckableTreeNode(id, this, model);
227                 treeNode.add(new AttributeModifier("title", getExternalDocumentServiceFacade().getDocumentDescription(
228                         getExternalDocumentServiceContext(), model.getObject(), getRequest().getLocale())));
229                 return treeNode;
230             }
231 
232             @Override
233             protected Item<Serializable> newRowItem(String id, int index, IModel<Serializable> model) {
234                 return new OddEvenItem<>(id, index, model);
235             }
236         };
237 
238         tree.getTable().addBottomToolbar(new NoRecordsToolbar(tree.getTable()));
239 
240         return tree;
241     }
242 
243     private List<IColumn<Serializable, String>> createColumns() {
244         List<IColumn<Serializable, String>> columns = new ArrayList<>();
245         columns.add(new TreeColumn<>(Model.of("Tree")));
246         return columns;
247     }
248 
249     private void expandExternalTreeItemNode(ExternalDocumentServiceFacade<Serializable> extDocService,
250             Serializable item, int curDepth, int maxDepth) {
251         if (curDepth < maxDepth) {
252             treeExpansionSet.add(item);
253 
254             if (extDocService.hasChildren(item)) {
255                 for (Iterator<Serializable> childIt = extDocService.getChildren(item); childIt.hasNext();) {
256                     expandExternalTreeItemNode(extDocService, childIt.next(), curDepth + 1, maxDepth);
257                 }
258             }
259         }
260     }
261 
262     private void expandPickedExternalTreeItemNodes(ExternalDocumentServiceFacade<Serializable> extDocService) {
263         for (Serializable item : getPickedExternalDocuments()) {
264             Serializable parent = extDocService.getParent(item);
265             while (parent != null) {
266                 treeExpansionSet.add(parent);
267                 parent = getExternalDocumentServiceFacade().getParent(parent);
268             }
269         }
270     }
271 
272     private class CheckableTreeNode extends CheckedFolder<Serializable> {
273 
274         private static final long serialVersionUID = 1L;
275 
276         private final AbstractTree<Serializable> tree;
277 
278         public CheckableTreeNode(String id, AbstractTree<Serializable> tree, IModel<Serializable> model) {
279             super(id, tree, model);
280             this.tree = tree;
281         }
282 
283         @Override
284         protected IModel<?> newLabelModel(IModel<Serializable> model) {
285             return Model.of(getExternalDocumentServiceFacade().getDocumentTitle(getExternalDocumentServiceContext(),
286                     model.getObject(), getRequest().getLocale()));
287         }
288 
289         @Override
290         protected Component newCheckBox(String id, IModel<Serializable> model) {
291             Component checkbox = super.newCheckBox(id, model);
292             checkbox.setEnabled(getExternalDocumentServiceFacade()
293                     .isDocumentSelectable(getExternalDocumentServiceContext(), model.getObject()));
294             return checkbox;
295         }
296 
297         @Override
298         protected IModel<Boolean> newCheckBoxModel(final IModel<Serializable> model) {
299             return new IModel<Boolean>() {
300                 private static final long serialVersionUID = 1L;
301 
302                 @Override
303                 public Boolean getObject() {
304                     return getPickedExternalDocuments().contains(model.getObject());
305                 }
306 
307                 @Override
308                 public void setObject(Boolean object) {
309                     if (Boolean.TRUE.equals(object)) {
310                         final boolean singleSelectionMode = isSingleSelectionMode();
311                         if (singleSelectionMode) {
312                             getPickedExternalDocuments().clear();
313                         }
314                         getPickedExternalDocuments().add(model.getObject());
315                     } else {
316                         getPickedExternalDocuments().remove(model.getObject());
317                     }
318                 }
319 
320                 @Override
321                 public void detach() {
322                 }
323             };
324         }
325 
326         @Override
327         protected void onUpdate(AjaxRequestTarget target) {
328             Serializable foo = getModelObject();
329             Serializable parent = treeDataProvider.getParent(foo);
330 
331             while (parent != null) {
332                 foo = parent;
333                 parent = treeDataProvider.getParent(parent);
334             }
335 
336             tree.updateBranch(foo, target);
337         }
338     }
339 }