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.cms.i18n.I18nContentSupport;
37  import info.magnolia.jcr.util.NodeTypes;
38  import info.magnolia.jcr.util.NodeUtil;
39  import info.magnolia.objectfactory.Components;
40  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
41  import info.magnolia.ui.form.field.definition.ConfiguredFieldDefinition;
42  import info.magnolia.ui.form.field.transformer.basic.BasicTransformer;
43  import info.magnolia.ui.vaadin.integration.jcr.JcrNewNodeAdapter;
44  import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
45  
46  import java.util.ArrayList;
47  import java.util.List;
48  import java.util.Locale;
49  
50  import javax.inject.Inject;
51  import javax.jcr.Node;
52  import javax.jcr.RepositoryException;
53  
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import com.google.common.collect.ImmutableList;
58  import com.vaadin.v7.data.Item;
59  import com.vaadin.v7.data.Property;
60  import com.vaadin.v7.data.util.ObjectProperty;
61  import com.vaadin.v7.data.util.PropertysetItem;
62  
63  /**
64   * This delegating {@link info.magnolia.ui.form.field.transformer.Transformer Transformer} is dedicated to the {@link info.magnolia.ui.form.field.MultiField MultiField};
65   * it considers entries as child nodes and delegates property handling to their respective sub-fields.
66   * <p>
67   * The storage strategy is that of the {@link info.magnolia.ui.form.field.transformer.multi.MultiValueChildNodeTransformer MultiValueChildNodeTransformer}:
68   * <ul>
69   * <li>rootItem (relatedFormItem)
70   * <ul>
71   * <li>childItem1 (first entry of the MultiField)<br>
72   * <li>childItem2 (second entry of the MultiField)<br>
73   * <li>...
74   * </ul>
75   * </ul>
76   */
77  public class DelegatingMultiValueFieldTransformer extends BasicTransformer<PropertysetItem> implements MultiTransformer {
78  
79      private static final Logger log = LoggerFactory.getLogger(DelegatingMultiValueFieldTransformer.class);
80  
81      protected String childNodeType = NodeTypes.ContentNode.NAME;
82  
83      protected String subItemBaseName;
84  
85      private List<String> delegateItemNames = new ArrayList<>();
86  
87      private PropertysetItem delegateAggregatorItem = new PropertysetItem();
88  
89  
90      /**
91       * @deprecated since 5.4.2 - use {@link #DelegatingMultiValueFieldTransformer(Item, ConfiguredFieldDefinition, Class, I18NAuthoringSupport)} instead.
92       */
93      @Deprecated
94      public DelegatingMultiValueFieldTransformer(Item relatedFormItem, ConfiguredFieldDefinition definition, Class<PropertysetItem> type, I18nContentSupport i18nContentSupport) {
95          this(relatedFormItem, definition, type, Components.getComponent(I18NAuthoringSupport.class));
96      }
97  
98      @Inject
99      public DelegatingMultiValueFieldTransformer(Item relatedFormItem, ConfiguredFieldDefinition definition, Class<PropertysetItem> type, I18NAuthoringSupport i18nAuthoringSupport) {
100         super(relatedFormItem, definition, type, i18nAuthoringSupport);
101         this.subItemBaseName = getSubItemBaseName();
102     }
103 
104     /**
105      * Returns a representation of the child items as a {@link PropertysetItem};
106      * 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}.
107      * <p>
108      * Please note that this list of child items is filtered based on the <i>subItemBaseName</i> and current locale.
109      */
110     @Override
111     public PropertysetItem readFromItem() {
112         // Only read it once
113         if (delegateAggregatorItem.getItemPropertyIds().isEmpty()) {
114             JcrNodeAdapter rootItem = getRootItem();
115             // The root Item was never populated, add relevant child Item based on the stored nodes.
116             if (!rootItem.hasChildItemChanges()) {
117                 populateStoredChildItems(rootItem);
118             }
119             // Get a list of childNodes
120             int position = 0;
121             for (String itemName : rootItem.getChildren().keySet()) {
122                 if (itemName.matches(childItemRegexRepresentation())) {
123                     delegateAggregatorItem.addItemProperty(position, new ObjectProperty<Item>(rootItem.getChild(itemName)));
124                     delegateItemNames.add(itemName);
125                     ++position;
126                 }
127             }
128         }
129         return delegateAggregatorItem;
130     }
131 
132     /**
133      * This transformer's write implementation is empty. We do not need to write to the item as this is delegated to the sub-fields.
134      */
135     @Override
136     public void writeToItem(PropertysetItem newValue) {
137         log.debug("CALL writeToItem");
138     }
139 
140     /**
141      * Creates a new child item, adds it to the root item, and returns it wrapped as an {@link ObjectProperty}.
142      * <p>
143      * The child item naming strategy is as follows: <i>subItemBaseName</i> + <i>increment</i> + <i>i18nSuffix</i>
144      *
145      * @see {@link #createNewItemName()}
146      */
147     @Override
148     public Property<?> createProperty() {
149         final String newItemName = createNewItemName();
150 
151         final JcrNodeAdapter child;
152 
153         // Should check the new Item Name existed because it can be created already from other language form.
154         if (getRootItem().getChild(newItemName) instanceof JcrNodeAdapter) {
155             child = (JcrNodeAdapter) getRootItem().getChild(newItemName);
156         } else {
157             child = new JcrNewNodeAdapter(getRootItem().getJcrItem(), childNodeType, newItemName);
158         }
159 
160         getRootItem().addChild(child);
161         Property<?> res = new ObjectProperty<Item>(child);
162         delegateAggregatorItem.addItemProperty(delegateAggregatorItem.getItemPropertyIds().size(), res);
163 
164         return res;
165     }
166 
167     @Override
168     public void removeProperty(Object id) {
169         Property<?> propertyToRemove = delegateAggregatorItem.getItemProperty(id);
170         if (propertyToRemove != null && propertyToRemove.getValue() != null) {
171             JcrNodeAdapter./../../../info/magnolia/ui/vaadin/integration/jcr/JcrNodeAdapter.html#JcrNodeAdapter">JcrNodeAdapter toRemove = (JcrNodeAdapter) propertyToRemove.getValue();
172             toRemove.getParent().removeChild(toRemove);
173         }
174         delegateAggregatorItem.removeItemProperty(id);
175         reorganizeIndex((Integer) id);
176     }
177 
178     @Override
179     public void setLocale(Locale locale) {
180         super.setLocale(locale);
181         ImmutableList<Object> propertyIds = ImmutableList.copyOf(delegateAggregatorItem.getItemPropertyIds());
182         for (Object id : propertyIds) {
183             delegateAggregatorItem.removeItemProperty(id);
184         }
185     }
186 
187     /**
188      * Ensure that id of the {@link PropertysetItem} stay coherent.<br>
189      * Assume that we have 3 values 0:a, 1:b, 2:c, and 1 is removed <br>
190      * If we just remove 1, the {@link PropertysetItem} will contain 0:a, 2:c, .<br>
191      * But we should have : 0:a, 1:c, .
192      */
193     private void reorganizeIndex(int fromIndex) {
194         int valuesSize = delegateAggregatorItem.getItemPropertyIds().size();
195         if (fromIndex == valuesSize) {
196             return;
197         }
198         while (fromIndex < valuesSize) {
199             int toIndex = fromIndex;
200             ++fromIndex;
201             delegateAggregatorItem.addItemProperty(toIndex, delegateAggregatorItem.getItemProperty(fromIndex));
202             delegateAggregatorItem.removeItemProperty(fromIndex);
203         }
204     }
205 
206     /**
207      * Defines the root item used to retrieve and create child items.
208      */
209     protected JcrNodeAdapter getRootItem() {
210         return (JcrNodeAdapter) relatedFormItem;
211     }
212 
213     /**
214      * Defines the base name to use for retrieving and creating child items.
215      * <p>
216      * By default, we use the {@link info.magnolia.ui.form.field.definition.FieldDefinition#getName()}.
217      */
218     protected String getSubItemBaseName() {
219         return definition.getName();
220     }
221 
222     /**
223      * Populates the given root item with its child items.
224      */
225     protected void populateStoredChildItems(JcrNodeAdapter rootItem) {
226         List<Node> childNodes = getStoredChildNodes(rootItem);
227         for (Node child : childNodes) {
228             JcrNodeAdapterntegration/jcr/JcrNodeAdapter.html#JcrNodeAdapter">JcrNodeAdapter item = new JcrNodeAdapter(child);
229             rootItem.addChild(item);
230         }
231     }
232 
233     /**
234      * Fetches child nodes of the given parent from JCR, filtered using the {@link NodeUtil#MAGNOLIA_FILTER} predicate.
235      */
236     protected List<Node> getStoredChildNodes(JcrNodeAdapter parent) {
237         try {
238             if (!(parent instanceof JcrNewNodeAdapter) && parent.getJcrItem().hasNodes()) {
239                 return NodeUtil.asList(NodeUtil.getNodes(parent.getJcrItem(), NodeUtil.MAGNOLIA_FILTER));
240             }
241         } catch (RepositoryException re) {
242             log.warn("Not able to access the Child Nodes of the following Node Identifier {}", parent.getItemId(), re);
243         }
244         return new ArrayList<Node>();
245     }
246 
247     /**
248      * Creates a unique name for the child item, in the following format:
249      * <i>subItemBaseName</i> + <i>increment</i> + <i>i18nSuffix</i>
250      * <ul>
251      * <li><i>subItemBaseName</i> by default we use the {@link info.magnolia.ui.form.field.definition.FieldDefinition#getName()}
252      * <li><i>increment</i> is the next available index for the current base name
253      * <li><i>i18nSuffix</i> is the default i18n suffix (typically something formatted like '_de')
254      * </ul>
255      * .
256      */
257     protected String createNewItemName() {
258         int increment = 0;
259         final List<String> childNodeNames = getChildItemNames();
260         String name;
261         do {
262             name = deriveLocaleAwareName(String.format("%s%d", subItemBaseName, increment));
263             ++increment;
264         } while (childNodeNames.contains(name));
265         return name;
266     }
267 
268     /**
269      * @return The regex used to filter child items based on i18n support and current locale
270      */
271     protected String childItemRegexRepresentation() {
272         if (hasI18NSupport()) {
273             if (getLocale() == null || getI18NAuthoringSupport().isDefaultLocale(getLocale(), relatedFormItem)) {
274                 // i18n set, current locale is the default locale
275                 // match all node name that do not define locale extension
276                 return subItemBaseName + incrementRegexRepresentation() + "((?!(_\\w{2}){1,3}))$";
277             } else {
278                 // i18n set, not default locale used
279                 return getI18NAuthoringSupport().deriveLocalisedPropertyName(subItemBaseName + incrementRegexRepresentation(), getLocale());
280             }
281         } else {
282             return subItemBaseName + incrementRegexRepresentation();
283         }
284     }
285 
286     protected String incrementRegexRepresentation() {
287         return "(\\d{1,3})";
288     }
289 
290     protected List<String> getChildItemNames() {
291         List<String> res = new ArrayList<>();
292         res.addAll(delegateItemNames);
293         for (Object delegateIds : delegateAggregatorItem.getItemPropertyIds()) {
294             Object value = delegateAggregatorItem.getItemProperty(delegateIds).getValue();
295             if (value instanceof JcrNodeAdapter) {
296                 res.add(((JcrNodeAdapter) value).getNodeName());
297             }
298         }
299         return res;
300     }
301 
302     /* I18nAwareHandler impl */
303 
304     @Override
305     public String getBasePropertyName() {
306         return subItemBaseName;
307     }
308 
309     @Override
310     public void setI18NPropertyName(String i18NSubNodeName) {
311         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.");
312     }
313 }