View Javadoc
1   /**
2    * This file Copyright (c) 2014-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.form.field.transformer.multi;
35  
36  import info.magnolia.annotation.deprecation.MgnlDeprecated;
37  
38  import info.magnolia.cms.i18n.I18nContentSupport;
39  import info.magnolia.jcr.util.NodeTypes;
40  import info.magnolia.jcr.util.NodeUtil;
41  import info.magnolia.objectfactory.Components;
42  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
43  import info.magnolia.ui.form.field.definition.ConfiguredFieldDefinition;
44  import info.magnolia.ui.form.field.transformer.basic.BasicTransformer;
45  import info.magnolia.ui.vaadin.integration.jcr.JcrNewNodeAdapter;
46  import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
47  
48  import java.util.ArrayList;
49  import java.util.List;
50  import java.util.Locale;
51  
52  import javax.inject.Inject;
53  import javax.jcr.Node;
54  import javax.jcr.RepositoryException;
55  
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import com.google.common.collect.ImmutableList;
60  import com.vaadin.v7.data.Item;
61  import com.vaadin.v7.data.Property;
62  import com.vaadin.v7.data.util.ObjectProperty;
63  import com.vaadin.v7.data.util.PropertysetItem;
64  
65  /**
66   * This delegating {@link info.magnolia.ui.form.field.transformer.Transformer Transformer} is dedicated to the {@link info.magnolia.ui.form.field.MultiField MultiField};
67   * it considers entries as child nodes and delegates property handling to their respective sub-fields.
68   * <p>
69   * The storage strategy is that of the {@link info.magnolia.ui.form.field.transformer.multi.MultiValueChildNodeTransformer MultiValueChildNodeTransformer}:
70   * <ul>
71   * <li>rootItem (relatedFormItem)
72   * <ul>
73   * <li>childItem1 (first entry of the MultiField)<br>
74   * <li>childItem2 (second entry of the MultiField)<br>
75   * <li>...
76   * </ul>
77   * </ul>
78   * @deprecated since 6.2.3. Use new framework and {@link info.magnolia.ui.editor.ByIndexedChildNodes} instead.
79   */
80  @Deprecated
81  @MgnlDeprecated(since = "6.2.3.", description = "Use new framework and ByIndexedChildNodes instead.")
82  public class DelegatingMultiValueFieldTransformer extends BasicTransformer<PropertysetItem> implements MultiTransformer {
83  
84      private static final Logger log = LoggerFactory.getLogger(DelegatingMultiValueFieldTransformer.class);
85  
86      protected String childNodeType = NodeTypes.ContentNode.NAME;
87  
88      protected String subItemBaseName;
89  
90      private List<String> delegateItemNames = new ArrayList<>();
91  
92      private PropertysetItem delegateAggregatorItem = new PropertysetItem();
93  
94  
95      /**
96       * @deprecated since 5.4.2 - use {@link #DelegatingMultiValueFieldTransformer(Item, ConfiguredFieldDefinition, Class, I18NAuthoringSupport)} instead.
97       */
98      @Deprecated
99      public DelegatingMultiValueFieldTransformer(Item relatedFormItem, ConfiguredFieldDefinition definition, Class<PropertysetItem> type, I18nContentSupport i18nContentSupport) {
100         this(relatedFormItem, definition, type, Components.getComponent(I18NAuthoringSupport.class));
101     }
102 
103     @Inject
104     public DelegatingMultiValueFieldTransformer(Item relatedFormItem, ConfiguredFieldDefinition definition, Class<PropertysetItem> type, I18NAuthoringSupport i18nAuthoringSupport) {
105         super(relatedFormItem, definition, type, i18nAuthoringSupport);
106         this.subItemBaseName = getSubItemBaseName();
107     }
108 
109     /**
110      * Returns a representation of the child items as a {@link PropertysetItem};
111      * this is merely a map whose keys are the positions in the <code>MultiField</code>, and whose values are the child items, wrapped as {@link ObjectProperty ObjectProperties}.
112      * <p>
113      * Please note that this list of child items is filtered based on the <i>subItemBaseName</i> and current locale.
114      */
115     @Override
116     public PropertysetItem readFromItem() {
117         // Only read it once
118         if (delegateAggregatorItem.getItemPropertyIds().isEmpty()) {
119             JcrNodeAdapter rootItem = getRootItem();
120             // The root Item was never populated, add relevant child Item based on the stored nodes.
121             if (!rootItem.hasChildItemChanges()) {
122                 populateStoredChildItems(rootItem);
123             }
124             // Get a list of childNodes
125             int position = 0;
126             for (String itemName : rootItem.getChildren().keySet()) {
127                 if (itemName.matches(childItemRegexRepresentation())) {
128                     delegateAggregatorItem.addItemProperty(position, new ObjectProperty<Item>(rootItem.getChild(itemName)));
129                     delegateItemNames.add(itemName);
130                     ++position;
131                 }
132             }
133         }
134         return delegateAggregatorItem;
135     }
136 
137     /**
138      * This transformer's write implementation is empty. We do not need to write to the item as this is delegated to the sub-fields.
139      */
140     @Override
141     public void writeToItem(PropertysetItem newValue) {
142         log.debug("CALL writeToItem");
143     }
144 
145     /**
146      * Creates a new child item, adds it to the root item, and returns it wrapped as an {@link ObjectProperty}.
147      * <p>
148      * The child item naming strategy is as follows: <i>subItemBaseName</i> + <i>increment</i> + <i>i18nSuffix</i>
149      *
150      * @see {@link #createNewItemName()}
151      */
152     @Override
153     public Property<?> createProperty() {
154         final String newItemName = createNewItemName();
155 
156         final JcrNodeAdapter child;
157 
158         // Should check the new Item Name existed because it can be created already from other language form.
159         if (getRootItem().getChild(newItemName) instanceof JcrNodeAdapter) {
160             child = (JcrNodeAdapter) getRootItem().getChild(newItemName);
161         } else {
162             child = new JcrNewNodeAdapter(getRootItem().getJcrItem(), childNodeType, newItemName);
163         }
164 
165         getRootItem().addChild(child);
166         Property<?> res = new ObjectProperty<Item>(child);
167         delegateAggregatorItem.addItemProperty(delegateAggregatorItem.getItemPropertyIds().size(), res);
168 
169         return res;
170     }
171 
172     @Override
173     public void removeProperty(Object id) {
174         Property<?> propertyToRemove = delegateAggregatorItem.getItemProperty(id);
175         if (propertyToRemove != null && propertyToRemove.getValue() != null) {
176             JcrNodeAdapter./../../../info/magnolia/ui/vaadin/integration/jcr/JcrNodeAdapter.html#JcrNodeAdapter">JcrNodeAdapter toRemove = (JcrNodeAdapter) propertyToRemove.getValue();
177             toRemove.getParent().removeChild(toRemove);
178         }
179         delegateAggregatorItem.removeItemProperty(id);
180         reorganizeIndex((Integer) id);
181     }
182 
183     @Override
184     public void setLocale(Locale locale) {
185         super.setLocale(locale);
186         ImmutableList<Object> propertyIds = ImmutableList.copyOf(delegateAggregatorItem.getItemPropertyIds());
187         for (Object id : propertyIds) {
188             delegateAggregatorItem.removeItemProperty(id);
189         }
190     }
191 
192     /**
193      * Ensure that id of the {@link PropertysetItem} stay coherent.<br>
194      * Assume that we have 3 values 0:a, 1:b, 2:c, and 1 is removed <br>
195      * If we just remove 1, the {@link PropertysetItem} will contain 0:a, 2:c, .<br>
196      * But we should have : 0:a, 1:c, .
197      */
198     private void reorganizeIndex(int fromIndex) {
199         int valuesSize = delegateAggregatorItem.getItemPropertyIds().size();
200         if (fromIndex == valuesSize) {
201             return;
202         }
203         while (fromIndex < valuesSize) {
204             int toIndex = fromIndex;
205             ++fromIndex;
206             delegateAggregatorItem.addItemProperty(toIndex, delegateAggregatorItem.getItemProperty(fromIndex));
207             delegateAggregatorItem.removeItemProperty(fromIndex);
208         }
209     }
210 
211     /**
212      * Defines the root item used to retrieve and create child items.
213      */
214     protected JcrNodeAdapter getRootItem() {
215         return (JcrNodeAdapter) relatedFormItem;
216     }
217 
218     /**
219      * Defines the base name to use for retrieving and creating child items.
220      * <p>
221      * By default, we use the {@link info.magnolia.ui.form.field.definition.FieldDefinition#getName()}.
222      */
223     protected String getSubItemBaseName() {
224         return definition.getName();
225     }
226 
227     /**
228      * Populates the given root item with its child items.
229      */
230     protected void populateStoredChildItems(JcrNodeAdapter rootItem) {
231         List<Node> childNodes = getStoredChildNodes(rootItem);
232         for (Node child : childNodes) {
233             JcrNodeAdapterntegration/jcr/JcrNodeAdapter.html#JcrNodeAdapter">JcrNodeAdapter item = new JcrNodeAdapter(child);
234             rootItem.addChild(item);
235         }
236     }
237 
238     /**
239      * Fetches child nodes of the given parent from JCR, filtered using the {@link NodeUtil#MAGNOLIA_FILTER} predicate.
240      */
241     protected List<Node> getStoredChildNodes(JcrNodeAdapter parent) {
242         try {
243             if (!(parent instanceof JcrNewNodeAdapter) && parent.getJcrItem().hasNodes()) {
244                 return NodeUtil.asList(NodeUtil.getNodes(parent.getJcrItem(), NodeUtil.MAGNOLIA_FILTER));
245             }
246         } catch (RepositoryException re) {
247             log.warn("Not able to access the Child Nodes of the following Node Identifier {}", parent.getItemId(), re);
248         }
249         return new ArrayList<Node>();
250     }
251 
252     /**
253      * Creates a unique name for the child item, in the following format:
254      * <i>subItemBaseName</i> + <i>increment</i> + <i>i18nSuffix</i>
255      * <ul>
256      * <li><i>subItemBaseName</i> by default we use the {@link info.magnolia.ui.form.field.definition.FieldDefinition#getName()}
257      * <li><i>increment</i> is the next available index for the current base name
258      * <li><i>i18nSuffix</i> is the default i18n suffix (typically something formatted like '_de')
259      * </ul>
260      * .
261      */
262     protected String createNewItemName() {
263         int increment = 0;
264         final List<String> childNodeNames = getChildItemNames();
265         String name;
266         do {
267             name = deriveLocaleAwareName(String.format("%s%d", subItemBaseName, increment));
268             ++increment;
269         } while (childNodeNames.contains(name));
270         return name;
271     }
272 
273     /**
274      * @return The regex used to filter child items based on i18n support and current locale
275      */
276     protected String childItemRegexRepresentation() {
277         if (hasI18NSupport()) {
278             if (getLocale() == null || getI18NAuthoringSupport().isDefaultLocale(getLocale(), relatedFormItem)) {
279                 // i18n set, current locale is the default locale
280                 // match all node name that do not define locale extension
281                 return subItemBaseName + incrementRegexRepresentation() + "((?!(_\\w{2}){1,3}))$";
282             } else {
283                 // i18n set, not default locale used
284                 return getI18NAuthoringSupport().deriveLocalisedPropertyName(subItemBaseName + incrementRegexRepresentation(), getLocale());
285             }
286         } else {
287             return subItemBaseName + incrementRegexRepresentation();
288         }
289     }
290 
291     protected String incrementRegexRepresentation() {
292         return "(\\d{1,3})";
293     }
294 
295     protected List<String> getChildItemNames() {
296         List<String> res = new ArrayList<>();
297         res.addAll(delegateItemNames);
298         for (Object delegateIds : delegateAggregatorItem.getItemPropertyIds()) {
299             Object value = delegateAggregatorItem.getItemProperty(delegateIds).getValue();
300             if (value instanceof JcrNodeAdapter) {
301                 res.add(((JcrNodeAdapter) value).getNodeName());
302             }
303         }
304         return res;
305     }
306 
307     /* I18nAwareHandler impl */
308 
309     @Override
310     public String getBasePropertyName() {
311         return subItemBaseName;
312     }
313 
314     @Override
315     public void setI18NPropertyName(String i18NSubNodeName) {
316         log.warn("DelegatingMultiValueFieldTransformer.setI18NPropertyName is deprecated since 5.4.2 - should you need a different locale-specific node name, it is possible to alter #i18nSuffix field in #setLocale() method.");
317     }
318 }