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;
17  
18  import java.io.Serializable;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.NoSuchElementException;
22  
23  import javax.jcr.Node;
24  
25  import org.apache.commons.lang3.StringUtils;
26  import org.apache.wicket.AttributeModifier;
27  import org.apache.wicket.MarkupContainer;
28  import org.apache.wicket.ajax.AjaxRequestTarget;
29  import org.apache.wicket.ajax.markup.html.AjaxLink;
30  import org.apache.wicket.behavior.AttributeAppender;
31  import org.apache.wicket.markup.head.CssHeaderItem;
32  import org.apache.wicket.markup.head.IHeaderResponse;
33  import org.apache.wicket.markup.html.WebMarkupContainer;
34  import org.apache.wicket.markup.html.basic.Label;
35  import org.apache.wicket.markup.html.list.ListItem;
36  import org.apache.wicket.markup.repeater.Item;
37  import org.apache.wicket.markup.repeater.RefreshingView;
38  import org.apache.wicket.markup.repeater.data.IDataProvider;
39  import org.apache.wicket.model.IModel;
40  import org.apache.wicket.model.Model;
41  import org.apache.wicket.model.StringResourceModel;
42  import org.apache.wicket.request.resource.PackageResourceReference;
43  import org.hippoecm.frontend.dialog.AbstractDialog;
44  import org.hippoecm.frontend.dialog.DialogLink;
45  import org.hippoecm.frontend.dialog.IDialogFactory;
46  import org.hippoecm.frontend.model.IModelReference;
47  import org.hippoecm.frontend.model.JcrNodeModel;
48  import org.hippoecm.frontend.model.event.IObservable;
49  import org.hippoecm.frontend.model.event.IObserver;
50  import org.hippoecm.frontend.plugin.IPluginContext;
51  import org.hippoecm.frontend.plugin.config.IPluginConfig;
52  import org.hippoecm.frontend.plugins.standards.diff.LCS;
53  import org.hippoecm.frontend.plugins.standards.diff.LCS.Change;
54  import org.hippoecm.frontend.plugins.standards.diff.LCS.ChangeType;
55  import org.hippoecm.frontend.plugins.standards.icon.HippoIcon;
56  import org.hippoecm.frontend.service.IEditor;
57  import org.hippoecm.frontend.service.render.RenderPlugin;
58  import org.hippoecm.frontend.skin.Icon;
59  import org.onehippo.forge.exdocpicker.api.ExternalDocumentCollection;
60  import org.onehippo.forge.exdocpicker.api.ExternalDocumentServiceContext;
61  import org.onehippo.forge.exdocpicker.api.ExternalDocumentServiceFacade;
62  import org.onehippo.forge.exdocpicker.api.PluginConstants;
63  import org.onehippo.forge.exdocpicker.impl.SimpleExternalDocumentCollectionDataProvider;
64  import org.onehippo.forge.exdocpicker.impl.SimpleExternalDocumentServiceContext;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  /**
69   * External document(s) selector field plugin.
70   */
71  @SuppressWarnings({ "unused", "rawtypes" })
72  public class ExternalDocumentFieldSelectorPlugin extends RenderPlugin<Node> implements IObserver {
73  
74      private static final long serialVersionUID = 1L;
75  
76      private static final Logger log = LoggerFactory.getLogger(ExternalDocumentFieldSelectorPlugin.class);
77  
78      private final JcrNodeModel documentModel;
79  
80      private ExternalDocumentServiceFacade<Serializable> exdocService;
81      private ExternalDocumentCollection<Serializable> curDocCollection;
82      private ExternalDocumentServiceContext extDocServiceContext;
83  
84      /**
85       * Constructs external document(s) selector field plugin.
86       * @param context plugin context
87       * @param config plugin config
88       */
89      @SuppressWarnings({ "unchecked" })
90      public ExternalDocumentFieldSelectorPlugin(final IPluginContext context, IPluginConfig config) {
91          super(context, config);
92  
93          setOutputMarkupId(true);
94  
95          documentModel = (JcrNodeModel)getModel();
96  
97          setExternalDocumentServiceFacade((ExternalDocumentServiceFacade<Serializable>) createExternalDocumentService());
98          setExternalDocumentServiceContext(new SimpleExternalDocumentServiceContext(this, config, context,
99                                                                                     documentModel));
100 
101         add(new Label("exdocfield-relateddocs-caption", getCaptionModel()));
102 
103         if (isEditMode() && documentModel != null) {
104             context.registerService(new IObserver() {
105                 public IObservable getObservable() {
106                     return documentModel;
107                 }
108 
109                 public void onEvent(Iterator events) {
110                     redraw();
111                 }
112             }, IObserver.class.getName());
113         }
114 
115         MarkupContainer exdocsContainer = new WebMarkupContainer("exdocs-container");
116 
117         boolean exdocsContainerVisible = getPluginConfig()
118             .getAsBoolean(PluginConstants.PARAM_EXTERNAL_DOCUMENTS_CONTAINER_VISIBLE, true);
119 
120         exdocsContainer.setVisible(exdocsContainerVisible);
121 
122         if (exdocsContainerVisible) {
123             if (!isCompareMode()) {
124                 exdocsContainer.add(createRefreshingView(getCurrentExternalDocumentCollection()));
125             } else {
126                 if (!getPluginConfig().containsKey("model.compareTo")) {
127                     log.warn("no base model service configured");
128                 } else {
129                     JcrNodeModel compareBaseDocumentModel = null;
130                     IModelReference compareBaseRef = getPluginContext()
131                         .getService(getPluginConfig().getString("model.compareTo"), IModelReference.class);
132 
133                     if (compareBaseRef == null) {
134                         log.warn("no base model service available");
135                     } else {
136                         compareBaseDocumentModel = new JcrNodeModel(((JcrNodeModel) compareBaseRef.getModel()).getItemModel().getPath());
137                         ExternalDocumentServiceContext comparingContext =
138                             new SimpleExternalDocumentServiceContext(this,
139                                                                      getPluginConfig(),
140                                                                      getPluginContext(),
141                                                                      compareBaseDocumentModel);
142                         ExternalDocumentCollection<Serializable> baseDocCollection = getExternalDocumentServiceFacade()
143                             .getFieldExternalDocuments(comparingContext);
144                         exdocsContainer.add(createCompareView(getCurrentExternalDocumentCollection(),
145                                                               baseDocCollection));
146                     }
147                 }
148             }
149         }
150 
151         add(exdocsContainer);
152 
153         WebMarkupContainer actionButtonsContainer = new WebMarkupContainer("action-buttons");
154         // Browse button
155         DialogLink browseButton = new DialogLink("browse-button",
156                 new StringResourceModel("picker.browse", this, null),
157                 createDialogFactory(),
158                 getDialogService());
159         actionButtonsContainer.add(browseButton);
160 
161         actionButtonsContainer.setVisible(isEditMode());
162         add(actionButtonsContainer);
163     }
164 
165     /**
166      * {@inheritDoc}
167      */
168     @Override
169     public void renderHead(IHeaderResponse response) {
170         super.renderHead(response);
171         response.render(CssHeaderItem
172             .forReference(new PackageResourceReference(ExternalDocumentFieldSelectorPlugin.class,
173                                                        ExternalDocumentFieldSelectorPlugin.class
174                                                            .getSimpleName() + ".css")));
175     }
176 
177     /**
178      * Returns true if it is currently in edit mode in the document editor.
179      * @return true if it is currently in edit mode in the document editor
180      */
181     protected boolean isEditMode() {
182         return IEditor.Mode.EDIT.equals(IEditor.Mode.fromString(getPluginConfig().getString("mode", "view")));
183     }
184 
185     /**
186      * Returns true if it is currently in compare mode in the document editor.
187      * @return true if it is currently in compare mode in the document editor
188      */
189     protected boolean isCompareMode() {
190         return IEditor.Mode.COMPARE.equals(IEditor.Mode.fromString(getPluginConfig()
191             .getString("mode", "view")));
192     }
193 
194     /**
195      * Creates a new {@link ExternalDocumentServiceFacade} instance.
196      * By default, this method reads the plugin configuration by the name, {@link PluginConstants#PARAM_EXTERNAL_DOCUMENT_SERVICE_FACADE},
197      * and instantiated an object by the FQCN configuration parameter.
198      * @return a new {@link ExternalDocumentServiceFacade} instance
199      */
200     @SuppressWarnings({ "unchecked" })
201     protected ExternalDocumentServiceFacade<? extends Serializable> createExternalDocumentService() {
202         ExternalDocumentServiceFacade<? extends Serializable> service = null;
203         String serviceFacadeClassName = null;
204 
205         try {
206             serviceFacadeClassName = getPluginConfig()
207                 .getString(PluginConstants.PARAM_EXTERNAL_DOCUMENT_SERVICE_FACADE);
208             Class<? extends ExternalDocumentServiceFacade> serviceClass = (Class<? extends ExternalDocumentServiceFacade>)Class
209                 .forName(serviceFacadeClassName);
210             service = serviceClass.getDeclaredConstructor().newInstance();
211         } catch (Exception e) {
212             log.error("Failed to create external document service facade from class name, '{}'.",
213                       serviceFacadeClassName, e);
214         }
215 
216         return service;
217     }
218 
219     /**
220      * Returns the field caption label.
221      * @return the field caption label
222      */
223     protected IModel<String> getCaptionModel() {
224         final String defaultCaption = new StringResourceModel("exdocfield.caption", this, null)
225                 .setDefaultValue(PluginConstants.DEFAULT_FIELD_CAPTION).getString();
226         String caption = getPluginConfig().getString("caption", defaultCaption);
227         return new StringResourceModel(caption, this, null).setDefaultValue(caption);
228     }
229 
230     @SuppressWarnings("unchecked")
231     private RefreshingView<? extends Serializable> createRefreshingView(final ExternalDocumentCollection<Serializable> docCollection) {
232         return new RefreshingView<Serializable>("view") {
233 
234             private static final long serialVersionUID = 1L;
235 
236             private final IDataProvider<Serializable> dataProvider =
237                 new SimpleExternalDocumentCollectionDataProvider<>(docCollection);
238 
239 
240             @Override
241             protected Iterator getItemModels() {
242 
243                 final Iterator<? extends Serializable> baseIt = dataProvider
244                     .iterator(0, docCollection.getSize());
245 
246                 return new Iterator<IModel<? extends Serializable>>() {
247                     public boolean hasNext() {
248                         return baseIt.hasNext();
249                     }
250 
251                     public IModel<? extends Serializable> next() {
252                         return dataProvider.model(baseIt.next());
253                     }
254 
255                     public void remove() {
256                         baseIt.remove();
257                     }
258                 };
259             }
260 
261             @Override
262             protected void populateItem(Item item) {
263                 final Serializable doc = (Serializable)item.getModelObject();
264 
265                 final Label label = new Label("link-text", new Model<String>() {
266                     private static final long serialVersionUID = 1L;
267 
268                     @Override
269                     public String getObject() {
270                         return getExternalDocumentServiceFacade()
271                             .getDocumentTitle(getExternalDocumentServiceContext(), doc,
272                                               getRequest().getLocale());
273                     }
274                 });
275                 final String description = getExternalDocumentServiceFacade()
276                         .getDocumentDescription(getExternalDocumentServiceContext(), doc, getRequest().getLocale());
277                 if (StringUtils.isNotBlank(description)) {
278                     label.add(new AttributeModifier("title", description));
279                 }
280                 item.add(label);
281 
282                 if (item.getIndex() == docCollection.getSize() - 1) {
283                     item.add(new AttributeAppender("class", new Model<>("last"), " "));
284                 }
285 
286                 addControlsToListItem(docCollection, item);
287             }
288         };
289     }
290 
291     private RefreshingView createCompareView(final ExternalDocumentCollection<Serializable> docCollection,
292                                              final ExternalDocumentCollection<Serializable> baseDocCollection) {
293 
294         return new RefreshingView("view") {
295 
296             @SuppressWarnings("unchecked")
297             @Override
298             protected Iterator getItemModels() {
299                 Serializable[] baseDocs = baseDocCollection.toArray(new Serializable[baseDocCollection
300                     .getSize()]);
301                 Serializable[] currentDocs = docCollection.toArray(new Serializable[docCollection.getSize()]);
302 
303                 List<Change<Serializable>> changeSet = LCS.getChangeSet(baseDocs, currentDocs);
304                 final Change<Serializable>[] changes = changeSet.toArray(new Change[changeSet.size()]);
305 
306                 return new Iterator<IModel<Change<? extends Serializable>>>() {
307 
308                     private int changeSetIndex = 0;
309 
310                     public boolean hasNext() {
311                         return changeSetIndex < changes.length;
312                     }
313 
314                     public IModel<Change<? extends Serializable>> next() {
315 
316                         if (changeSetIndex >= changes.length) {
317                             throw new NoSuchElementException();
318                         }
319 
320                         final Change<? extends Serializable> change = changes[changeSetIndex++];
321 
322                         return new Model<>(change);
323                     }
324 
325                     public void remove() {
326                         throw new UnsupportedOperationException();
327                     }
328                 };
329             }
330 
331             @SuppressWarnings("unchecked")
332             @Override
333             protected void populateItem(Item item) {
334                 Change<? extends Serializable> change = (Change<? extends Serializable>)item.getModelObject();
335                 final Serializable searchDoc = change.getValue();
336 
337                 final Label label = new Label("link-text", new Model<String>() {
338                     private static final long serialVersionUID = 1L;
339 
340                     @Override
341                     public String getObject() {
342                         return getExternalDocumentServiceFacade()
343                             .getDocumentTitle(getExternalDocumentServiceContext(), searchDoc,
344                                               getRequest().getLocale());
345                     }
346                 });
347 
348                 final String description = getExternalDocumentServiceFacade().getDocumentDescription(
349                         getExternalDocumentServiceContext(), searchDoc, getRequest().getLocale());
350                 if (StringUtils.isNotBlank(description)) {
351                     label.add(new AttributeModifier("title", description));
352                 }
353 
354                 if (change.getType() == ChangeType.ADDED) {
355                     label.add(new AttributeAppender("class", new Model<>("hippo-diff-added"), " "));
356                 } else if (change.getType() == ChangeType.REMOVED) {
357                     label.add(new AttributeAppender("class", new Model<>("hippo-diff-removed"), " "));
358                 }
359 
360                 item.add(label);
361 
362                 addControlsToListItem(docCollection, item);
363             }
364         };
365     }
366 
367     /**
368      * Returns the {@link ExternalDocumentServiceFacade} instance used by this plugin instance.
369      * @return the {@link ExternalDocumentServiceFacade} instance used by this plugin instance
370      */
371     protected ExternalDocumentServiceFacade<Serializable> getExternalDocumentServiceFacade() {
372         return exdocService;
373     }
374 
375     /**
376      * Sets the {@link ExternalDocumentServiceFacade} instance used by this plugin instance.
377      * @param exdocService the {@link ExternalDocumentServiceFacade} instance used by this plugin instance
378      */
379     protected void setExternalDocumentServiceFacade(ExternalDocumentServiceFacade<Serializable> exdocService) {
380         this.exdocService = exdocService;
381     }
382 
383     /**
384      * Returns the collection of the currently-selected external documents in the document.
385      * @return the collection of the currently-selected external documents in the document
386      */
387     protected ExternalDocumentCollection<Serializable> getCurrentExternalDocumentCollection() {
388         if (curDocCollection == null) {
389             curDocCollection = getExternalDocumentServiceFacade()
390                 .getFieldExternalDocuments(getExternalDocumentServiceContext());
391         }
392 
393         return curDocCollection;
394     }
395 
396     /**
397      * Sets the collection of the currently-selected external documents in the document.
398      * @param curDocCollection the collection of the currently-selected external documents in the document
399      */
400     protected void setCurrentExternalDocumentCollection(ExternalDocumentCollection<Serializable> curDocCollection) {
401         this.curDocCollection = curDocCollection;
402     }
403 
404     /**
405      * Returns the {@link ExternalDocumentServiceContext} instance.
406      * @return the {@link ExternalDocumentServiceContext} instance
407      */
408     protected ExternalDocumentServiceContext getExternalDocumentServiceContext() {
409         return extDocServiceContext;
410     }
411 
412     /**
413      * Sets the {@link ExternalDocumentServiceContext} instance.
414      * @param extDocServiceContext the {@link ExternalDocumentServiceContext} instance
415      */
416     protected void setExternalDocumentServiceContext(ExternalDocumentServiceContext extDocServiceContext) {
417         this.extDocServiceContext = extDocServiceContext;
418     }
419 
420     /**
421      * Creates a picker dialog with which user can select external documents.
422      * @return a picker dialog with which user can select external documents
423      */
424     protected AbstractDialog<ExternalDocumentCollection<Serializable>> createDialogInstance() {
425         return new TextSearchExternalDocumentFieldBrowserDialog(
426                 getCaptionModel(),
427                 getExternalDocumentServiceContext(),
428                 getExternalDocumentServiceFacade(),
429                 new Model<ExternalDocumentCollection<Serializable>>(getCurrentExternalDocumentCollection()));
430     }
431 
432     /**
433      * Creates a dialog factory to create a picker dialog.
434      * @return a dialog factory to create a picker dialog
435      */
436     protected IDialogFactory createDialogFactory() {
437         return new IDialogFactory() {
438             private static final long serialVersionUID = 1L;
439 
440             public AbstractDialog<ExternalDocumentCollection<Serializable>> createDialog() {
441                 return createDialogInstance();
442             }
443         };
444     }
445 
446     private void addControlsToListItem(final ExternalDocumentCollection<Serializable> docCollection, final ListItem<?> item) {
447         final Serializable doc = (Serializable)item.getModelObject();
448 
449         final boolean isEditMode = isEditMode();
450         final int itemCount = docCollection.getSize();
451         final int itemIndex = item.getIndex();
452 
453         final WebMarkupContainer controls = new WebMarkupContainer("controls");
454         controls.setVisible(isEditMode);
455 
456         final MarkupContainer upLink = new AjaxLink<Void>("up") {
457             @Override
458             public void onClick(AjaxRequestTarget target) {
459                 boolean removed = docCollection.remove(doc);
460                 if (removed) {
461                     docCollection.add(itemIndex - 1, doc);
462                     exdocService.setFieldExternalDocuments(extDocServiceContext, docCollection);
463                     target.add(ExternalDocumentFieldSelectorPlugin.this);
464                 }
465             }
466         };
467         upLink.setEnabled(isEditMode && itemIndex > 0);
468         upLink.setVisible(isEditMode);
469         final HippoIcon upIcon = HippoIcon.fromSprite("up-icon", Icon.ARROW_UP);
470         upLink.add(upIcon);
471         controls.add(upLink);
472 
473         final MarkupContainer downLink = new AjaxLink<Void>("down") {
474             @Override
475             public void onClick(AjaxRequestTarget target) {
476                 boolean removed = docCollection.remove(doc);
477                 if (removed) {
478                     docCollection.add(itemIndex + 1, doc);
479                     exdocService.setFieldExternalDocuments(extDocServiceContext, docCollection);
480                     target.add(ExternalDocumentFieldSelectorPlugin.this);
481                 }
482             }
483         };
484         downLink.setEnabled(isEditMode && itemIndex < itemCount - 1);
485         downLink.setVisible(isEditMode);
486         final HippoIcon downIcon = HippoIcon.fromSprite("down-icon", Icon.ARROW_DOWN);
487         downLink.add(downIcon);
488         controls.add(downLink);
489 
490         final MarkupContainer removeLink = new AjaxLink<Void>("remove") {
491             @Override
492             public void onClick(AjaxRequestTarget target) {
493                 boolean removed = docCollection.remove(doc);
494                 if (removed) {
495                     exdocService.setFieldExternalDocuments(extDocServiceContext, docCollection);
496                     target.add(ExternalDocumentFieldSelectorPlugin.this);
497                 }
498             }
499         };
500         removeLink.setEnabled(isEditMode);
501         removeLink.setVisible(isEditMode);
502         final HippoIcon removeIcon = HippoIcon.fromSprite("remove-icon", Icon.TIMES);
503         removeLink.add(removeIcon);
504         controls.add(removeLink);
505 
506         item.add(controls);
507     }
508 }