View Javadoc
1   /**
2    * This file Copyright (c) 2014-2015 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.vaadin.data.Item;
58  import com.vaadin.data.Property;
59  import com.vaadin.data.util.ObjectProperty;
60  import com.vaadin.data.util.PropertysetItem;
61  
62  /**
63   * This delegating {@link info.magnolia.ui.form.field.transformer.Transformer Transformer} is dedicated to the {@link info.magnolia.ui.form.field.MultiField MultiField};
64   * it considers entries as child nodes and delegates property handling to their respective sub-fields.
65   * <p>
66   * The storage strategy is that of the {@link info.magnolia.ui.form.field.transformer.multi.MultiValueChildNodeTransformer MultiValueChildNodeTransformer}:
67   * <ul>
68   * <li>rootItem (relatedFormItem)
69   * <ul>
70   * <li>childItem1 (first entry of the MultiField)<br>
71   * <li>childItem2 (second entry of the MultiField)<br>
72   * <li>...
73   * </ul>
74   * </ul>
75   */
76  public class DelegatingMultiValueFieldTransformer extends BasicTransformer<PropertysetItem> implements MultiTransformer {
77  
78      private static final Logger log = LoggerFactory.getLogger(DelegatingMultiValueFieldTransformer.class);
79  
80      protected String childNodeType = NodeTypes.ContentNode.NAME;
81  
82      protected String subItemBaseName;
83  
84      private List<String> delegateItemNames = new ArrayList<>();
85  
86      private PropertysetItem delegateAggregatorItem = new PropertysetItem();
87  
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         child.setParent(getRootItem());
161         child.getParent().addChild(child);
162         Property<?> res = new ObjectProperty<Item>(child);
163         delegateAggregatorItem.addItemProperty(delegateAggregatorItem.getItemPropertyIds().size(), res);
164 
165         return res;
166     }
167 
168     @Override
169     public void removeProperty(Object id) {
170         Property<?> propertyToRemove = delegateAggregatorItem.getItemProperty(id);
171         if (propertyToRemove != null && propertyToRemove.getValue() != null) {
172             JcrNodeAdapter toRemove = (JcrNodeAdapter) propertyToRemove.getValue();
173             toRemove.getParent().removeChild(toRemove);
174         }
175         delegateAggregatorItem.removeItemProperty(id);
176         reorganizeIndex((Integer) id);
177     }
178 
179     @Override
180     public void setLocale(Locale locale) {
181         super.setLocale(locale);
182         for (Object id : delegateAggregatorItem.getItemPropertyIds()) {
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             JcrNodeAdapter item = new JcrNodeAdapter(child);
229             item.setParent(rootItem);
230             item.getParent().addChild(item);
231         }
232     }
233 
234     /**
235      * Fetches child nodes of the given parent from JCR, filtered using the {@link NodeUtil#MAGNOLIA_FILTER} predicate.
236      */
237     protected List<Node> getStoredChildNodes(JcrNodeAdapter parent) {
238         try {
239             if (!(parent instanceof JcrNewNodeAdapter) && parent.getJcrItem().hasNodes()) {
240                 return NodeUtil.asList(NodeUtil.getNodes(parent.getJcrItem(), NodeUtil.MAGNOLIA_FILTER));
241             }
242         } catch (RepositoryException re) {
243             log.warn("Not able to access the Child Nodes of the following Node Identifier {}", parent.getItemId(), re);
244         }
245         return new ArrayList<Node>();
246     }
247 
248     /**
249      * Creates a unique name for the child item, in the following format:
250      * <i>subItemBaseName</i> + <i>increment</i> + <i>i18nSuffix</i>
251      * <ul>
252      * <li><i>subItemBaseName</i> by default we use the {@link info.magnolia.ui.form.field.definition.FieldDefinition#getName()}
253      * <li><i>increment</i> is the next available index for the current base name
254      * <li><i>i18nSuffix</i> is the default i18n suffix (typically something formatted like '_de')
255      * </ul>
256      * .
257      */
258     protected String createNewItemName() {
259         int increment = 0;
260         final List<String> childNodeNames = getChildItemNames();
261         String name;
262         do {
263             name = deriveLocaleAwareName(String.format("%s%d", subItemBaseName, increment));
264             ++increment;
265         } while (childNodeNames.contains(name));
266         return name;
267     }
268 
269     /**
270      * @return The regex used to filter child items based on i18n support and current locale
271      */
272     protected String childItemRegexRepresentation() {
273         if (hasI18NSupport()) {
274             if (getLocale() == null || getI18NAuthoringSupport().isDefaultLocale(getLocale(), relatedFormItem)) {
275                 // i18n set, current locale is the default locale
276                 // match all node name that do not define locale extension
277                 return subItemBaseName + incrementRegexRepresentation() + "((?!(_\\w{2}){1,3}))$";
278             } else {
279                 // i18n set, not default locale used
280                 return getI18NAuthoringSupport().deriveLocalisedPropertyName(subItemBaseName + incrementRegexRepresentation(), getLocale());
281             }
282         } else {
283             return subItemBaseName + incrementRegexRepresentation();
284         }
285     }
286 
287     protected String incrementRegexRepresentation() {
288         return "(\\d{1,3})";
289     }
290 
291     private List<String> getChildItemNames() {
292         List<String> res = new ArrayList<>();
293         res.addAll(delegateItemNames);
294         for (Object delegateIds : delegateAggregatorItem.getItemPropertyIds()) {
295             Object value = delegateAggregatorItem.getItemProperty(delegateIds).getValue();
296             if (value instanceof JcrNodeAdapter) {
297                 res.add(((JcrNodeAdapter) value).getNodeName());
298             }
299         }
300         return res;
301     }
302 
303     /* I18nAwareHandler impl */
304 
305     @Override
306     public String getBasePropertyName() {
307         return subItemBaseName;
308     }
309 
310     @Override
311     public void setI18NPropertyName(String i18NSubNodeName) {
312         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.");
313     }
314 }