View Javadoc

1   /**
2    * This file Copyright (c) 2003-2012 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.module.groovy.support.nodes;
35  
36  import groovy.lang.DelegatingMetaClass;
37  import groovy.lang.GroovySystem;
38  import groovy.lang.MetaClass;
39  import groovy.xml.QName;
40  
41  import info.magnolia.cms.core.Content;
42  import info.magnolia.cms.core.ItemType;
43  import info.magnolia.cms.core.MgnlNodeType;
44  import info.magnolia.cms.core.NodeData;
45  import info.magnolia.cms.security.AccessDeniedException;
46  import info.magnolia.cms.util.ContentWrapper;
47  import info.magnolia.jcr.RuntimeRepositoryException;
48  
49  import java.math.BigDecimal;
50  import java.util.ArrayList;
51  import java.util.Collection;
52  import java.util.Iterator;
53  import java.util.List;
54  
55  import javax.jcr.PathNotFoundException;
56  import javax.jcr.PropertyType;
57  import javax.jcr.RepositoryException;
58  import javax.jcr.Value;
59  import javax.jcr.ValueFormatException;
60  
61  import org.apache.commons.collections.IteratorUtils;
62  import org.apache.commons.lang.StringUtils;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  /**
67   * A special <em>groovish</em> implementation of Magnolia's {@link ContentWrapper}.
68   * This makes it possible, for example, navigating the nodes in a Magnolia repository
69   * with a <code>. (dot)</code> notation and access their properties with <code>.</code> or <code>.@</code> notation much like it happens when using the result of parsing an xml with groovy's {@link groovy.util.XmlSlurper.XmlSlurper}. For example, here is how, in a groovy
70   * script, one could navigate to and print the node data named <em>abstract</em>:
71   *
72   * <pre>
73   * hm = ctx.getHierarchyManager('website')
74   * node = hm.getContent(hm, '/demo-project')
75   * println node.about.history.abstract
76   * </pre>
77   *
78   * The above example can also be made more groovy-like by using the <code>@</code> notation to access the attribute
79   *
80   * <pre>
81   * node.about.history.@abstract
82   * </pre>
83   *
84   * As <code>node</code> is also a {@link Content}, you can call any of its
85   * methods. E.g.
86   *
87   * <pre>
88   * println node.metaData.template
89   * println node.about.children.title
90   * println node.about.parent <strong>(this can return null)</strong>
91   * </pre>
92   *
93   * <strong>IMPORTANT:</strong> The <code>.children</code> shortcut, unlikely {@link Content#getChildren()}, <strong>always returns {@link ItemType#CONTENT}s AND {@link ItemType#CONTENTNODE}s </strong>.
94   * If you only need either type, then call directly one of the {@link Content#getChildren()} methods on the parent node. <br/>
95   * <br/>
96   * If you want to look at the <i>attributes</i> or <i>data</i> for a certain node you can simply call
97   *
98   * <pre>
99   * println node.about.nodeData
100  * </pre>
101  *
102  * It is also possible to assign values to node data or create new ones. E.g.
103  *
104  * <pre>
105  * node.boo = 3.14d
106  * node.coo = true
107  * node.foo = 'some text'
108  * node.doo = 100
109  * </pre>
110  *
111  * will assign the values on the right hand side to the node data on left hand side. Should those not exist, they will be automatically created.
112  * Furthermore, the correct type will be detected based on the value assigned (i.e. Boolean, String, Long or Double). <br/>
113  * <br/>
114  * <strong>IMPORTANT:</strong> All the above assignments will be in-memory only unless explicitly persisted via a call to <code>save()</code> on a parent node.
115  */
116 public class MgnlGroovyNode extends ContentWrapper {
117 
118     static {
119         // wrap the standard MetaClass with the delegate
120         setMetaClass(GroovySystem.getMetaClassRegistry().getMetaClass(MgnlGroovyNode.class), MgnlGroovyNode.class);
121     }
122 
123     protected static final Logger log = LoggerFactory.getLogger(MgnlGroovyNode.class);
124 
125     private String name;
126 
127     public MgnlGroovyNode(Content content) {
128         super(content);
129         this.name = content.getName();
130     }
131 
132     protected static void setMetaClass(final MetaClass metaClass, Class nodeClass) {
133         final MetaClass newMetaClass = new DelegatingMetaClass(metaClass) {
134 
135             @Override
136             public Object getAttribute(final Object object, final String attribute) {
137                 MgnlGroovyNode n = (MgnlGroovyNode) object;
138                 return n.get("@" + attribute);
139             }
140 
141             @Override
142             public void setAttribute(final Object object, final String attribute, final Object newValue) {
143                 MgnlGroovyNode n = (MgnlGroovyNode) object;
144                 try {
145                     n.setNodeData(attribute, newValue);
146                 }
147                 catch (AccessDeniedException e) {
148                     log.error(e.getMessage());
149                 }
150                 catch (PathNotFoundException e) {
151                     log.error(e.getMessage());
152                 }
153                 catch (RepositoryException e) {
154                     log.error(e.getMessage());
155                 }
156             }
157 
158             @Override
159             public Object getProperty(Object object, String property) {
160                 if (object instanceof MgnlGroovyNode) {
161                     MgnlGroovyNode n = (MgnlGroovyNode) object;
162                     return n.get(property);
163                 }
164                 return super.getProperty(object, property);
165             }
166 
167             @Override
168             public void setProperty(Object object, String property, Object newValue) {
169                 String attribute = null;
170                 if (property.startsWith("@")) {
171                     attribute = property.substring(1);
172                 } else {
173                     attribute = property;
174                 }
175                 MgnlGroovyNode n = (MgnlGroovyNode) object;
176                 try {
177                     // else you get java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long
178                     if (newValue instanceof Integer) {
179                         newValue = ((Integer) newValue).longValue();
180                     }
181                     n.setNodeData(attribute, newValue);
182                 }
183                 catch (AccessDeniedException e) {
184                     log.error(e.getMessage());
185                 }
186                 catch (PathNotFoundException e) {
187                     log.error(e.getMessage());
188                 }
189                 catch (RepositoryException e) {
190                     log.error(e.getMessage());
191                 }
192             }
193         };
194         GroovySystem.getMetaClassRegistry().setMetaClass(nodeClass, newMetaClass);
195     }
196 
197     public Iterator<Content> iterator() {
198         return getChainedChildrenIterator();
199     }
200 
201     public String name() {
202         return name;
203     }
204 
205     /**
206      * Provides lookup of elements by non-namespaced name.
207      *
208      * @param key the name (or shortcut key) of the node(s) of interest
209      * @return the nodes which match key
210      */
211     public Object get(String key) {
212         if (key != null && key.charAt(0) == '@') {
213             String attributeName = key.substring(1);
214             return getNodeDataValue(attributeName);
215         }
216         if ("parent".equals(key)) {
217             try {
218                 return getParent();
219             } catch (RepositoryException e) {
220                 return null;
221             }
222         }
223         if ("children".equals(key)) {
224             Collection<Content> retVal = getChildren();
225             if (!isContentNodeType()) {
226                 retVal.addAll(getChildren(MgnlNodeType.NT_CONTENTNODE));
227             }
228             return retVal;
229         }
230         if ("metaData".equals(key)) {
231             return getMetaData();
232         }
233         if ("nodeData".equals(key)) {
234             return getNodeDataCollection();
235         }
236         if ("name".equals(key)) {
237             return name;
238         }
239         Content retVal = getByName(key);
240         if (retVal != null) {
241             return retVal;
242         }
243         // last attempt: lets assume the client requested for an attribute without the @ notation
244         return getNodeDataValue(key);
245     }
246 
247     @Override
248     protected Content wrap(Content node) {
249         return new MgnlGroovyNode(node);
250     }
251 
252     /**
253      * Returns the value for the given attribute (nodeData).
254      * The Java types returned vary according to the underlying {@link PropertyType}.
255      * <ul>
256      * <li> {@link PropertyType#STRING} returns a {@link String}
257      * <li> {@link PropertyType#DATE} returns a {@link java.util.Calendar}
258      * <li> {@link PropertyType#DOUBLE} returns a {@link BigDecimal}
259      * <li> {@link PropertyType#LONG} returns a {@link BigDecimal}
260      * <li> {@link PropertyType#BINARY} returns the {@link info.magnolia.cms.core.BinaryNodeData} value.
261      * <li>All other property types will be returned as {@link String}(s)
262      * </ul>
263      */
264     protected Object getNodeDataValue(String attributeName) {
265         Collection<NodeData> data = getNodeDataCollection(attributeName);
266         Iterator<NodeData> it = data.iterator();
267         if (!it.hasNext()) {
268             return null;
269         }
270         NodeData nodeData = it.next();
271         Value propertyValue = nodeData.getValue();
272         Object value = null;
273         if (propertyValue != null) {
274             try {
275                 switch (propertyValue.getType()) {
276                 case PropertyType.STRING:
277                     value = propertyValue.getString();
278                     break;
279                 case PropertyType.BINARY:
280                     value = propertyValue;
281                     break;
282                 case PropertyType.DATE:
283                     value = propertyValue.getDate();
284                     break;
285                 case PropertyType.DOUBLE:
286                     value = BigDecimal.valueOf(propertyValue.getDouble());
287                     break;
288                 case PropertyType.LONG:
289                     value = BigDecimal.valueOf(propertyValue.getLong());
290                     break;
291                 default:
292                     value = propertyValue.getString();
293                 }
294             } catch (ValueFormatException e) {
295                 log.warn(e.getMessage());
296             } catch (IllegalStateException e) {
297                 log.warn(e.getMessage());
298             } catch (RepositoryException e) {
299                 log.warn(e.getMessage());
300             }
301         }
302         return value;
303     }
304 
305     /**
306      * Provides lookup of elements by QName.
307      *
308      * @param name the QName of interest
309      * @return the nodes matching name
310      * @deprecated This method is wrong and luckily unused by this module. Use {@link MgnlGroovyNode#getByName(String)} instead.
311      */
312     @Deprecated
313     public List<Content> getAt(QName name) {
314         List<Content> answer = new ArrayList<Content>();
315         for (Iterator<Content> iter = getChainedChildrenIterator(); iter.hasNext();) {
316             Content childNode = iter.next();
317             Object childNodeName = childNode.getName();
318             if (name.matches(childNodeName)) {
319                 answer.add(childNode);
320             }
321         }
322         return answer;
323     }
324 
325     /**
326      * Provides lookup of elements by name.
327      *
328      * @param name the name of interest
329      * @return the nodes matching name
330      */
331     protected Content getByName(String name) {
332         for (Iterator<Content> iter = getChainedChildrenIterator(); iter.hasNext();) {
333             Content childNode = iter.next();
334             String childNodeName = childNode.getName();
335             if (name.equals(childNodeName)) {
336                 return childNode;
337             }
338         }
339         return null;
340     }
341 
342     @Override
343     public String toString() {
344         StringBuilder retVal = new StringBuilder();
345         Iterator<Content> iterator = getChainedChildrenIterator();
346         retVal.append("(+) " + name + "\n");
347         while (iterator.hasNext()) {
348             Content c = iterator.next();
349             if (hasMoreChildren(c)) {
350                 retVal.append("\t(+) ");
351             } else {
352                 retVal.append("\t(-) ");
353             }
354             retVal.append(c.getName() + "\n");
355         }
356 
357         if (!getNodeDataCollection().isEmpty()) {
358             for (NodeData nodeData : getNodeDataCollection()) {
359                 retVal.append("\t\t* " + nodeData.getName() + ": ["
360                         + StringUtils.abbreviate(nodeData.getString(), 80)
361                         + "]\n");
362             }
363 
364         }
365 
366         return retVal.toString();
367     }
368 
369     @SuppressWarnings("unchecked")
370     private Iterator<Content> getChainedChildrenIterator() {
371         if (isContentNodeType()) {
372             return getChildren().iterator();
373         }
374         return IteratorUtils.chainedIterator(getChildren().iterator(), getChildren(ItemType.CONTENTNODE).iterator());
375     }
376 
377     private boolean hasMoreChildren(Content c) {
378         return c.hasChildren() || c.hasChildren(ItemType.CONTENTNODE.getSystemName());
379     }
380 
381     @Override
382     public int hashCode() {
383         final int prime = 31;
384         int result = 1;
385         result = prime * result + ((name == null) ? 0 : name.hashCode());
386         return result;
387     }
388 
389     @Override
390     public boolean equals(Object obj) {
391         if (this == obj)
392             return true;
393         if (obj == null)
394             return false;
395         if (getClass() != obj.getClass())
396             return false;
397         MgnlGroovyNode other = (MgnlGroovyNode) obj;
398         if (name == null) {
399             if (other.name != null)
400                 return false;
401         } else if (!getHandle().equals(other.getHandle()))
402             return false;
403         return true;
404     }
405 
406     private boolean isContentNodeType() {
407         try {
408             return MgnlNodeType.NT_CONTENTNODE.equals(getItemType().getSystemName());
409 
410         } catch (RepositoryException e) {
411             throw new RuntimeRepositoryException(e);
412         }
413     }
414 
415 }