View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 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.admininterface;
35  
36  import info.magnolia.cms.beans.config.URI2RepositoryManager;
37  import info.magnolia.cms.beans.config.URI2RepositoryMapping;
38  import info.magnolia.cms.beans.runtime.Document;
39  import info.magnolia.cms.beans.runtime.FileProperties;
40  import info.magnolia.cms.beans.runtime.MultipartForm;
41  import info.magnolia.cms.core.Content;
42  import info.magnolia.cms.core.HierarchyManager;
43  import info.magnolia.cms.core.ItemType;
44  import info.magnolia.cms.core.MgnlNodeType;
45  import info.magnolia.cms.core.NodeData;
46  import info.magnolia.cms.core.Path;
47  import info.magnolia.cms.gui.control.ControlImpl;
48  import info.magnolia.cms.gui.control.File;
49  import info.magnolia.cms.gui.fckeditor.FCKEditorTmpFiles;
50  import info.magnolia.cms.security.AccessDeniedException;
51  import info.magnolia.cms.security.Digester;
52  import info.magnolia.cms.util.ContentUtil;
53  import info.magnolia.cms.util.DateUtil;
54  import info.magnolia.cms.util.ExclusiveWrite;
55  import info.magnolia.cms.util.NodeDataUtil;
56  import info.magnolia.context.MgnlContext;
57  import info.magnolia.link.LinkUtil;
58  import info.magnolia.objectfactory.Classes;
59  import info.magnolia.objectfactory.Components;
60  import info.magnolia.objectfactory.MgnlInstantiationException;
61  import info.magnolia.repository.RepositoryConstants;
62  
63  import java.io.FileInputStream;
64  import java.io.FileNotFoundException;
65  import java.io.IOException;
66  import java.io.InputStream;
67  import java.util.ArrayList;
68  import java.util.Calendar;
69  import java.util.Collection;
70  import java.util.GregorianCalendar;
71  import java.util.Iterator;
72  import java.util.List;
73  import java.util.Map;
74  import java.util.TimeZone;
75  import java.util.regex.Matcher;
76  import java.util.regex.Pattern;
77  
78  import javax.jcr.Node;
79  import javax.jcr.PathNotFoundException;
80  import javax.jcr.PropertyType;
81  import javax.jcr.RepositoryException;
82  import javax.jcr.Value;
83  import javax.jcr.ValueFactory;
84  
85  import org.apache.commons.codec.binary.Base64;
86  import org.apache.commons.io.FileUtils;
87  import org.apache.commons.io.IOUtils;
88  import org.apache.commons.lang.StringUtils;
89  import org.devlib.schmidt.imageinfo.ImageInfo;
90  import org.slf4j.Logger;
91  import org.slf4j.LoggerFactory;
92  
93  
94  /**
95   * <p>
96   * This class handles the saving in the dialogs. It uses the mgnlSaveInfo parameters sent from the browser to store
97   * the data in the node.The structure of the parameter is the following: <br>
98   * <code>name, type, valueType, isRichEditValue, encoding</code>
99   * </p>
100  * <p>To find the consts see ControlImpl</p>
101  * <table>
102  * <tr>
103  * <td>name</td>
104  * <td>the name of the field</td>
105  * </tr>
106  * <tr>
107  * <td>type</td>
108  * <td>string, boolean, ...</td>
109  * </tr>
110  * <tr>
111  * <td>valueType</td>
112  * <td>single, multiple</td>
113  * </tr>
114  * <tr>
115  * <td>isRichEditValue</td>
116  * <td>value from an editor</td>
117  * </tr>
118  * <tr>
119  * <td>encoding</td>
120  * <td>base64, unix, none</td>
121  * </tr>
122  * </table>
123  * <p>
124  * You can specify a custom save handled (a class that implements info.magnolia.module.admininterface.CustomSaveHandler)
125  * by adding a "saveHandler" property to a dialog field configuration.
126  * </p>
127  * @author Vinzenz Wyser
128  * @version 2.0
129  */
130 public class SaveHandlerImpl implements SaveHandler {
131 
132     /**
133      * Logger
134      */
135     public static Logger log = LoggerFactory.getLogger(SaveHandlerImpl.class);
136 
137     /**
138      * The from, containing all the fields and files. This form is generated by magnolia.
139      */
140     private MultipartForm form;
141 
142     /**
143      * creates the node if it is not present
144      */
145     private boolean create;
146 
147     private ItemType creationItemType = ItemType.CONTENTNODE;
148     private ItemType collectionNodeCreationItemType = ItemType.CONTENTNODE;
149 
150     /**
151      * The name of the repository to store the data. Website is default.
152      */
153     private String repository = RepositoryConstants.WEBSITE;
154 
155     /**
156      * Path to the page
157      */
158     private String path;
159 
160     /**
161      * If the saved paragraph is saved to a node collection
162      */
163     private String nodeCollectionName;
164 
165     /**
166      * The name of the node saving to. mgnlNew if a new node should get creaetd
167      */
168     private String nodeName;
169 
170     /**
171      * The current paragraph type. This information is saved under the node to enable a proper rendering.
172      */
173     private String paragraph;
174 
175     /**
176      * Call init to initialize the object
177      */
178     public SaveHandlerImpl() {
179     }
180 
181     /**
182      * Initialize the SaveHandlerImpl control.
183      * @param form the form generated from the request due to handle multipart forms
184      */
185     @Override
186     public void init(MultipartForm form) {
187         this.setForm(form);
188     }
189 
190     /**
191      * @see info.magnolia.module.admininterface.SaveHandler#save()
192      */
193     @Override
194     public boolean save() {
195 
196         String[] saveInfos = getForm().getParameterValues("mgnlSaveInfo"); // name,type,propertyOrNode
197 
198         if (saveInfos == null) {
199             log.info("Nothing to save, mgnlSaveInfo parameter not found.");
200             return true;
201         }
202 
203         synchronized (ExclusiveWrite.getInstance()) {
204             // //$NON-NLS-1$
205             String path = this.getPath();
206 
207             HierarchyManager hm = MgnlContext.getHierarchyManager(this.getRepository());
208             try {
209                 // get the node to save
210                 Content page = this.getPageNode(hm);
211 
212                 if (page == null) {
213                     // an error should have been logged in getPageNode() avoid NPEs!
214                     return false;
215                 }
216 
217                 Content node = this.getSaveNode(hm, page);
218 
219                 // this value can get used later on to find this node
220                 this.setNodeName(node.getName());
221                 if (StringUtils.isEmpty(node.getMetaData().getTemplate())) {
222                     node.getMetaData().setTemplate(this.getParagraph());
223                 }
224 
225                 // loop all saveInfo controls; saveInfo format: name, type, valueType(single|multiple, )
226                 for (int i = 0; i < saveInfos.length; i++) {
227                     String saveInfo = saveInfos[i];
228                     processSaveInfo(node, saveInfo);
229                 }
230 
231                 // deleting all documents
232                 try {
233                     MultipartForm form = getForm();
234                     Map docs = form.getDocuments();
235                     Iterator iter = docs.keySet().iterator();
236                     while (iter.hasNext()) {
237                         form.getDocument((String) iter.next()).delete();
238                     }
239                 }
240                 catch (Exception e) {
241                     log.error("Could not delete temp documents from form");
242                 }
243 
244                 log.debug("Saving {}", path); //$NON-NLS-1$
245 
246                 // update meta data (e.g. last modified) of this paragraph and the page
247                 updateMetaData(node);
248 
249                 // FIX for MAGNOLIA-1814
250                 // mark page as changed also for nested paragraph changes
251                 if (this.getRepository().equals(RepositoryConstants.WEBSITE)) {
252                     while (node.getItemType().equals(ItemType.CONTENTNODE)) {
253                         node = node.getParent();
254                         node.updateMetaData();
255                     }
256                 }
257 
258                 hm.save();
259             }
260             catch (RepositoryException re) {
261                 log.error(re.getMessage(), re);
262                 return false;
263             }
264         }
265         return true;
266     }
267 
268     /**
269      * Update recursively the node Metadata until the parent node is of type
270      * mgnl:content or mgnl:file or deph=1.
271      * If it's not the 'website' workspace, do not perform recursivity.
272      */
273     private void updateMetaData(Content currentNode) throws AccessDeniedException, RepositoryException {
274         if(currentNode.isNodeType(MgnlNodeType.NT_FOLDER)) {
275             return;
276         }
277         // Update
278         currentNode.updateMetaData();
279         // Break or perform a recursive call
280         if(!RepositoryConstants.WEBSITE.equals(currentNode.getWorkspace().getName()) || currentNode.isNodeType(MgnlNodeType.NT_CONTENT) || currentNode.isNodeType(MgnlNodeType.NT_FOLDER) || currentNode.getLevel()<2) {
281             return;
282         } else {
283             updateMetaData(currentNode.getParent());
284         }
285     }
286 
287     /**
288      * This method cares about one mgnlSaveInfo. It adds the value to the node
289      * @param node node to add data
290      * @param saveInfo <code>name, type, valueType, isRichEditValue, encoding</code>
291      * @throws PathNotFoundException exception
292      * @throws RepositoryException exception
293      * @throws AccessDeniedException no access
294      */
295     protected void processSaveInfo(Content node, String saveInfo) throws PathNotFoundException, RepositoryException,
296     AccessDeniedException {
297 
298         String name;
299         int type = PropertyType.STRING;
300         int valueType = ControlImpl.VALUETYPE_SINGLE;
301         int isRichEditValue = 0;
302         int encoding = ControlImpl.ENCRYPTION_NO;
303         String[] values = {StringUtils.EMPTY};
304         if (StringUtils.contains(saveInfo, ',')) {
305             String[] info = StringUtils.split(saveInfo, ',');
306             name = info[0];
307             if (info.length >= 2) {
308                 type = PropertyType.valueFromName(info[1]);
309             }
310             if (info.length >= 3) {
311                 valueType = Integer.valueOf(info[2]).intValue();
312             }
313             if (info.length >= 4) {
314                 isRichEditValue = Integer.valueOf(info[3]).intValue();
315             }
316             if (info.length >= 5) {
317                 encoding = Integer.valueOf(info[4]).intValue();
318             }
319         }
320         else {
321             name = saveInfo;
322         }
323 
324         // handing of custom save handler start
325         String customSaveHandler = this.getForm().getParameter(name + "_saveHandler");
326         if (!StringUtils.isEmpty(customSaveHandler)) {
327             try {
328                 Class cshClazz = Classes.getClassFactory().forName(customSaveHandler);
329                 if (!FieldSaveHandler.class.isAssignableFrom(cshClazz)) {
330                     log.error("Class {} must implement the FieldSaveHandler interface", cshClazz);
331                     throw new ClassCastException("Class " + cshClazz + " must implement the FieldSaveHandler interface");
332                 }
333                 else {
334                     FieldSaveHandler csh = (FieldSaveHandler)Components.newInstance(cshClazz);
335                     HierarchyManager hm = MgnlContext.getSystemContext().getHierarchyManager(RepositoryConstants.CONFIG);
336 
337                     String configPath = this.getForm().getParameter(name + "_configNode");
338                     Content configNode = null;
339                     if (StringUtils.isNotEmpty(configPath))
340                     {
341                         configNode = hm.getContent(configPath);
342                     }
343 
344                     csh.save(
345                             node,
346                             configNode,
347                             name,
348                             getForm(),
349                             type,
350                             valueType,
351                             isRichEditValue,
352                             encoding);
353                 }
354             }
355             catch (ClassNotFoundException e) {
356                 log.error("Error loading class " + customSaveHandler, e);
357             }
358             catch (MgnlInstantiationException e) {
359                 log.error("Error creating instance of class " + customSaveHandler, e);
360             }
361         }
362         else
363             // handing of custom save handler end
364 
365             if (type == PropertyType.BINARY) {
366                 processBinary(node, name);
367             }
368             else {
369                 values = getForm().getParameterValues(name);
370                 if (valueType == ControlImpl.VALUETYPE_MULTIPLE) {
371                     processMultiple(node, name, type, values);
372                 }
373                 else if (isRichEditValue != ControlImpl.RICHEDIT_NONE) {
374                     processRichEdit(node, name, type, isRichEditValue, encoding, values);
375                 }
376                 else if (type == PropertyType.DATE) {
377                     processDate(node, name, type, valueType, encoding, values);
378                 }
379                 else {
380                     processCommon(node, name, type, valueType, encoding, values);
381                 }
382             }
383     }
384 
385     protected void processDate(Content node, String name, int type, int valueType, int encoding, String[] values) {
386         try {
387             if (StringUtils.isEmpty(values[0])) {
388                 log.debug("Date has no value. Deleting node data {}", name);
389                 if (node.hasNodeData(name)) {
390                     node.deleteNodeData(name);
391                 }
392             }
393             else {
394                 Calendar utc = DateUtil.getUTCCalendarFromDialogString(values[0]);
395                 NodeDataUtil.getOrCreate(node, name).setValue(utc);
396             }
397         }
398         catch (Exception e) {
399             log.error("Could not update date value of node:" + node.getHandle() + " of property:" + name, e);
400         }
401     }
402 
403     /**
404      * Parse the value returned by a rich text editor and update the links and linebreaks.
405      * @param node
406      * @param name
407      * @param type
408      * @param isRichEditValue
409      * @param encoding
410      * @param values
411      * @throws PathNotFoundException
412      * @throws RepositoryException
413      * @throws AccessDeniedException
414      */
415     protected void processRichEdit(Content node, String name, int type, int isRichEditValue, int encoding,
416             String[] values) throws PathNotFoundException, RepositoryException, AccessDeniedException {
417         String valueStr = StringUtils.EMPTY;
418         if (values != null) {
419             valueStr = values[0]; // values is null when the expected field would not exis, e.g no
420         }
421 
422         valueStr = cleanLineBreaks(valueStr, isRichEditValue);
423 
424         if (isRichEditValue == ControlImpl.RICHEDIT_FCK) {
425             // MAGNOLIA-3384: remove scayt <span> http://dev.ckeditor.com/ticket/3560
426             valueStr = valueStr.replaceAll("<span scaytid=\"\\d*\" scayt_word=\".*?\">(.*?)</span>", "$1");
427             valueStr = updateLinks(node, name, valueStr);
428         }
429 
430         processString(node, name, type, encoding, values, valueStr);
431     }
432 
433     /**
434      * Clean up the linebreaks and
435      * <p>
436      * , <br>
437      * tags returned by the rich text editors
438      * @param valueStr
439      * @return the cleaned string
440      */
441     protected String cleanLineBreaks(String valueStr, int isRichEditValue) {
442         valueStr = StringUtils.replace(valueStr, "\r\n", " "); //$NON-NLS-1$ //$NON-NLS-2$
443         valueStr = StringUtils.replace(valueStr, "\n", " "); //$NON-NLS-1$ //$NON-NLS-2$
444 
445         // ie inserts some strange br...
446         valueStr = StringUtils.replace(valueStr, "</br>", StringUtils.EMPTY); //$NON-NLS-1$
447         valueStr = StringUtils.replace(valueStr, "<br>", "<br/>"); //$NON-NLS-1$ //$NON-NLS-2$
448         valueStr = StringUtils.replace(valueStr, "<BR>", "<br />"); //$NON-NLS-1$ //$NON-NLS-2$
449         valueStr = StringUtils.replace(valueStr, "<br/>", "<br />"); //$NON-NLS-1$ //$NON-NLS-2$
450         valueStr = StringUtils.replace(valueStr, "<P><br />", "<P>"); //$NON-NLS-1$ //$NON-NLS-2$
451 
452         // replace <P>
453         if (isRichEditValue != ControlImpl.RICHEDIT_FCK) {
454             valueStr = replacePByBr(valueStr, "p"); //$NON-NLS-1$
455         }
456         return valueStr;
457     }
458 
459     /**
460      * Update the links in a string returned by a rich text editor. If there are links to temporary files created due
461      * the fckeditor upload mechanism those filese are written to the node.
462      * @param node node saving to. used to save the files and fileinfod to
463      * @param name the name of the field. used to make a subnode for the files
464      * @param valueStr the value containing the links
465      * @return the cleaned value
466      * @throws AccessDeniedException
467      * @throws RepositoryException
468      * @throws PathNotFoundException
469      */
470     protected String updateLinks(Content node, String name, String valueStr) throws AccessDeniedException,
471     RepositoryException, PathNotFoundException {
472 
473         // process the images and uploaded files
474         HierarchyManager hm = MgnlContext.getHierarchyManager(this.getRepository());
475 
476         Pattern imageOrDowloadPattern = Pattern.compile("(<(a|img|embed)[^>]+(href|src)[ ]*=[ ]*\")([^\"]*)(\"[^>]*>)");
477         Pattern tmpFilePattern = Pattern.compile("/tmp/fckeditor/([^/]*)/[^\"]*");
478 
479         Content filesNode = ContentUtil.getOrCreateContent(node, name + "_files", ItemType.CONTENTNODE);
480 
481         // not usedFiles are removed after saving
482         List usedFiles = new ArrayList();
483 
484         // adapt img urls
485         Matcher imageOrDowloadMatcher = imageOrDowloadPattern.matcher(valueStr);
486         StringBuffer res = new StringBuffer();
487         while (imageOrDowloadMatcher.find()) {
488             String src = imageOrDowloadMatcher.group(4);
489 
490             String link = StringUtils.removeStart(src, MgnlContext.getContextPath());
491 
492             // process the tmporary uploaded files
493             Matcher tmpFileMatcher = tmpFilePattern.matcher(src);
494 
495             if (tmpFileMatcher.find()) {
496                 String uuid = tmpFileMatcher.group(1);
497 
498                 Document doc = FCKEditorTmpFiles.getDocument(uuid);
499                 String fileNodeName = Path.getUniqueLabel(hm, filesNode.getHandle(), "file");
500                 Content fileNode = ContentUtil.getOrCreateContent(filesNode, fileNodeName, ItemType.CONTENTNODE);
501                 SaveHandlerImpl.saveDocument(fileNode, doc, "document", "", "");
502                 link = filesNode.getHandle() + "/" + fileNodeName + "/document/" + doc.getFileNameWithExtension();
503                 doc.delete();
504                 try {
505                     FileUtils.deleteDirectory(new java.io.File(Path.getTempDirectory() + "/fckeditor/" + uuid));
506                 }
507                 catch (IOException e) {
508                     log.error("can't delete tmp file [" + Path.getTempDirectory() + "/fckeditor/" + uuid + "]");
509                 }
510             }
511 
512             String prefixRepo = "";
513             String mappedUri = "";
514             URI2RepositoryManager uri2RepositoryManager = URI2RepositoryManager.getInstance();
515             if (this.repository.equals(uri2RepositoryManager.getDefaultMapping().getRepository())) {
516                 prefixRepo = uri2RepositoryManager.getDefaultMapping().getURIPrefix();
517             } else {
518                 Collection<URI2RepositoryMapping> mappings = uri2RepositoryManager.getMappings();
519                 for (URI2RepositoryMapping mapping : mappings) {
520                     if (mapping.getRepository().equals(this.repository)) {
521                         prefixRepo = mapping.getURIPrefix();
522                         if(!StringUtils.isEmpty(mapping.getHandlePrefix()) && link.startsWith(prefixRepo)){
523                             mappedUri = mapping.getURI(filesNode.getHandle());
524                             mappedUri = StringUtils.substringBeforeLast(mappedUri, ".");
525                         }
526                         break;
527                     }
528                 }
529             }
530 
531             prefixRepo = StringUtils.removeEnd(prefixRepo, "/");
532 
533             link = cleanupLinkPrefix(filesNode, link, prefixRepo);
534 
535             // internal uuid links have a leading $
536             link = StringUtils.replace(link, "$", "\\$");
537 
538             imageOrDowloadMatcher.appendReplacement(res, "$1" + link + "$5"); //$NON-NLS-1$
539 
540             if (link.startsWith(prefixRepo + filesNode.getHandle())) {
541                 String fileNodeName = StringUtils.removeStart(link, prefixRepo + filesNode.getHandle() + "/");
542                 fileNodeName = StringUtils.substringBefore(fileNodeName, "/");
543                 usedFiles.add(fileNodeName);
544             }else if (!StringUtils.isEmpty(mappedUri) && link.startsWith(mappedUri)){
545                 String fileNodeName = StringUtils.removeStart(link, mappedUri + "/");
546                 fileNodeName = StringUtils.substringBefore(fileNodeName, "/");
547                 usedFiles.add(fileNodeName);
548             }
549         }
550 
551         // delete not used files
552         for (Iterator iter = filesNode.getChildren().iterator(); iter.hasNext();) {
553             Content fileNodeData = (Content) iter.next();
554             if (!usedFiles.contains(fileNodeData.getName())) {
555                 fileNodeData.delete();
556             }
557         }
558 
559         imageOrDowloadMatcher.appendTail(res);
560         valueStr = res.toString();
561 
562         // encode the internal links to avoid dependences from the contextpath, position of the page
563         valueStr = LinkUtil.convertAbsoluteLinksToUUIDs(valueStr);
564 
565         return valueStr;
566     }
567 
568     protected String cleanupLinkPrefix(Content filesNode, String link, String prefixRepo) {
569         if (link.contains(filesNode.getHandle()) && !link.startsWith(prefixRepo + "/")) {
570             link = prefixRepo + link;
571         }
572         return link;
573     }
574 
575     /**
576      * Process a common value
577      * @param node node where the data must be stored
578      * @param name name of the field
579      * @param type type
580      * @param valueType internal value type (according to ControlImpl)
581      * @param encoding must we encode (base64)
582      * @param values all values belonging to this field
583      * @throws PathNotFoundException exception
584      * @throws RepositoryException exception
585      * @throws AccessDeniedException exception
586      */
587     protected void processCommon(Content node, String name, int type, int valueType, int encoding, String[] values)
588     throws PathNotFoundException, RepositoryException, AccessDeniedException {
589         String valueStr = StringUtils.EMPTY;
590         if (values != null) {
591             valueStr = values[0]; // values is null when the expected field would not exis, e.g no
592         }
593 
594         processString(node, name, type, encoding, values, valueStr);
595     }
596 
597     /**
598      * Process a string. This method will encode it
599      * @param node
600      * @param name
601      * @param type
602      * @param encoding
603      * @param values
604      * @param valueStr
605      * @throws PathNotFoundException
606      * @throws RepositoryException
607      * @throws AccessDeniedException
608      */
609     protected void processString(Content node, String name, int type, int encoding, String[] values, String valueStr)
610     throws PathNotFoundException, RepositoryException, AccessDeniedException {
611         // actualy encoding does only work for control password
612         boolean remove = false;
613         boolean write = false;
614         if (encoding == ControlImpl.ENCRYPTION_NO_ENCODING_BASE64) {
615             if (StringUtils.isNotBlank(valueStr)) {
616                 valueStr = new String(Base64.encodeBase64(valueStr.getBytes()));
617                 write = true;
618             }
619         } else if (encoding == ControlImpl.ENCRYPTION_HASH_SHA) {
620             if (StringUtils.isNotEmpty(valueStr)) {
621                 valueStr = Digester.getSHA1Hex(valueStr);
622                 write = true;
623             }
624         } else if (encoding == ControlImpl.ENCRYPTION_HASH_BCRYPT) {
625             if (StringUtils.isNotEmpty(valueStr)) {
626                 valueStr = Digester.getBCrypt(valueStr);
627                 write = true;
628             }
629         } else {
630             // no encoding
631             if (values == null || StringUtils.isEmpty(valueStr)) {
632                 remove = true;
633             } else {
634                 write = true;
635             }
636         }
637         if (remove) {
638             processRemoveCommon(node, name);
639         }
640         else if (write) {
641             processWriteCommon(node, name, valueStr, type);
642         }
643     }
644 
645     /**
646      * Remove the specified property on the node.
647      */
648     protected void processRemoveCommon(Content node, String name) throws PathNotFoundException, RepositoryException {
649         NodeData data = node.getNodeData(name);
650 
651         if (data.isExist()) {
652             node.deleteNodeData(name);
653         }
654     }
655 
656     /**
657      * Writes a property value.
658      * @param node the node
659      * @param name the property name to be written
660      * @param valueStr the value of the property
661      * @throws AccessDeniedException thrown if the write access is not granted
662      * @throws RepositoryException thrown if other repository exception is thrown
663      */
664     protected void processWriteCommon(Content node, String name, String valueStr, int type)
665     throws AccessDeniedException, RepositoryException {
666         Value value = this.getValue(valueStr, type);
667         if (null != value) {
668             NodeData data = NodeDataUtil.getOrCreate(node, name);
669             data.setValue(value);
670         }
671     }
672 
673     /**
674      * Process a multiple value field
675      * @param node
676      * @param name
677      * @param type
678      * @param values
679      * @throws RepositoryException
680      * @throws PathNotFoundException
681      * @throws AccessDeniedException
682      */
683     protected void processMultiple(Content node, String name, int type, String[] values) throws RepositoryException,
684     PathNotFoundException, AccessDeniedException {
685         // remove entire content node and (re-)write each
686         try {
687             node.delete(name);
688         }
689         catch (PathNotFoundException e) {
690             if (log.isDebugEnabled()) {
691                 log.debug("Exception caught: " + e.getMessage(), e); //$NON-NLS-1$
692             }
693         }
694         if (values != null && values.length != 0) {
695             Content multiNode = node.createContent(name, ItemType.CONTENTNODE);
696             try {
697                 // MetaData.CREATION_DATE has private access; no method to delete it so far...
698                 multiNode.deleteNodeData("creationdate"); //$NON-NLS-1$
699             }
700             catch (RepositoryException re) {
701                 if (log.isDebugEnabled()) {
702                     log.debug("Exception caught: " + re.getMessage(), re); //$NON-NLS-1$
703                 }
704             }
705             for (int j = 0; j < values.length; j++) {
706                 String valueStr = values[j];
707                 if (StringUtils.isNotEmpty(valueStr)) {
708                     Value value = this.getValue(valueStr, type);
709                     multiNode.createNodeData(Integer.toString(j)).setValue(value);
710                 }
711             }
712         }
713     }
714 
715     /**
716      * Process binary data. File- or imageupload.
717      * @param node
718      * @param name
719      * @throws PathNotFoundException
720      * @throws RepositoryException
721      * @throws AccessDeniedException
722      */
723     protected void processBinary(Content node, String name) throws PathNotFoundException, RepositoryException,
724     AccessDeniedException {
725         Document doc = getForm().getDocument(name);
726         if (doc == null && getForm().getParameter(name + "_" + File.REMOVE) != null) { //$NON-NLS-1$
727             try {
728                 node.deleteNodeData(name);
729             }
730             catch (RepositoryException re) {
731                 if (log.isDebugEnabled()) {
732                     log.debug("Exception caught: " + re.getMessage(), re); //$NON-NLS-1$
733                 }
734             }
735         }
736         else {
737             String fileName = getForm().getParameter(name + "_" + FileProperties.PROPERTY_FILENAME);
738             String template = getForm().getParameter(name + "_" + FileProperties.PROPERTY_TEMPLATE);
739 
740             SaveHandlerImpl.saveDocument(node, doc, name, fileName, template);
741         }
742     }
743 
744     /**
745      * Get a string value
746      * @param s
747      * @return the value
748      */
749     public Value getValue(String s) {
750         return this.getValue(s, PropertyType.STRING);
751     }
752 
753     /**
754      * Get the long value
755      * @param l
756      * @return the value
757      */
758     public Value getValue(long l) {
759         HierarchyManager hm = MgnlContext.getHierarchyManager(this.getRepository());
760         ValueFactory valueFactory;
761         try {
762             valueFactory = hm.getWorkspace().getSession().getValueFactory();
763         }
764         catch (RepositoryException e) {
765             throw new RuntimeException(e);
766         }
767         return valueFactory.createValue(l);
768     }
769 
770     /**
771      * Get the value for saving in jcr
772      * @param valueStr string representation of the value
773      * @param type type of the value
774      * @return the value
775      */
776     public Value getValue(String valueStr, int type) {
777 
778         ValueFactory valueFactory = null;
779 
780         HierarchyManager hm = MgnlContext.getHierarchyManager(this.getRepository());
781         try {
782             valueFactory = hm.getWorkspace().getSession().getValueFactory();
783         }
784         catch (RepositoryException e) {
785             throw new RuntimeException(e);
786         }
787 
788         Value value = null;
789 
790         if (type == PropertyType.REFERENCE) {
791             try {
792                 Node referencedNode = hm.getWorkspace().getSession().getNodeByUUID(valueStr);
793 
794                 value = valueFactory.createValue(referencedNode);
795             }
796             catch (RepositoryException re) {
797                 if (log.isDebugEnabled()) {
798                     log.debug("Cannot retrieve the referenced node by UUID: " + valueStr, re);
799                 }
800             }
801         }
802         else {
803             value = NodeDataUtil.createValue(valueStr, type, valueFactory);
804         }
805 
806         return value;
807     }
808 
809     /**
810      * @param value
811      * @param tagName
812      */
813     protected static String replacePByBr(final String value, String tagName) {
814 
815         if (StringUtils.isBlank(value)) {
816             return value;
817         }
818 
819         String fixedValue = value;
820 
821         String pre = "<" + tagName + ">"; //$NON-NLS-1$ //$NON-NLS-2$
822         String post = "</" + tagName + ">"; //$NON-NLS-1$ //$NON-NLS-2$
823 
824         // get rid of last </p>
825         if (fixedValue.endsWith(post)) {
826             fixedValue = StringUtils.substringBeforeLast(fixedValue, post);
827         }
828 
829         fixedValue = StringUtils.replace(fixedValue, pre + "&nbsp;" + post, "\n "); //$NON-NLS-1$ //$NON-NLS-2$
830         fixedValue = StringUtils.replace(fixedValue, pre, StringUtils.EMPTY);
831         fixedValue = StringUtils.replace(fixedValue, post, "\n\n "); //$NON-NLS-1$
832 
833         if (!tagName.equals(tagName.toUpperCase())) {
834             fixedValue = replacePByBr(fixedValue, tagName.toUpperCase());
835         }
836         return fixedValue;
837     }
838 
839     /**
840      * @see info.magnolia.module.admininterface.SaveHandler#isCreate()
841      */
842     @Override
843     public boolean isCreate() {
844         return create;
845     }
846 
847     /**
848      * @see info.magnolia.module.admininterface.SaveHandler#setCreate(boolean)
849      */
850     @Override
851     public void setCreate(boolean create) {
852         this.create = create;
853     }
854 
855     /**
856      * @see info.magnolia.module.admininterface.SaveHandler#getCreationItemType()
857      */
858     @Override
859     public ItemType getCreationItemType() {
860         return creationItemType;
861     }
862 
863     /**
864      * @see info.magnolia.module.admininterface.SaveHandler#setCreationItemType(info.magnolia.cms.core.ItemType)
865      */
866     @Override
867     public void setCreationItemType(ItemType creationItemType) {
868         this.creationItemType = creationItemType;
869     }
870 
871     @Override
872     public ItemType getCollectionNodeCreationItemType() {
873         return collectionNodeCreationItemType;
874     }
875 
876     @Override
877     public void setCollectionNodeCreationItemType(ItemType collectionNodeCreationItemType) {
878         this.collectionNodeCreationItemType = collectionNodeCreationItemType;
879     }
880 
881     /**
882      * @return the form containing the values passed
883      */
884     protected MultipartForm getForm() {
885         return form;
886     }
887 
888     /**
889      * set the from
890      * @param form containing the sended values
891      */
892     protected void setForm(MultipartForm form) {
893         this.form = form;
894     }
895 
896     /**
897      * set the name of the repository saving to
898      * @param repository the name of the repository
899      */
900     @Override
901     public void setRepository(String repository) {
902         this.repository = repository;
903     }
904 
905     /**
906      * get the name of thre repository saving to
907      * @return name
908      */
909     @Override
910     public String getRepository() {
911         return repository;
912     }
913 
914     /**
915      * Returns the page. The page is created if not yet existing depending on the property create
916      * @param hm
917      * @return the node
918      * @throws RepositoryException
919      * @throws AccessDeniedException
920      * @throws PathNotFoundException
921      */
922     protected Content getPageNode(HierarchyManager hm) throws RepositoryException, AccessDeniedException,
923     PathNotFoundException {
924         Content page = null;
925         String path = this.getPath();
926         try {
927             page = hm.getContent(path);
928         }
929         catch (RepositoryException e) {
930             if (this.isCreate()) {
931                 String parentPath = StringUtils.substringBeforeLast(path, "/"); //$NON-NLS-1$
932                 String label = StringUtils.substringAfterLast(path, "/"); //$NON-NLS-1$
933                 if (StringUtils.isEmpty(parentPath)) {
934                     page = hm.getRoot();
935                 }
936                 else {
937                     page = hm.getContent(parentPath);
938                 }
939                 page = page.createContent(label, this.getCreationItemType());
940             }
941             else {
942                 log.error("Tried to save a not existing node with path {}. use create = true to force creation", path); //$NON-NLS-1$
943             }
944         }
945 
946         return page;
947     }
948 
949     /**
950      * Gets or creates the node saving to.
951      * @param hm
952      * @param rootNode the node containing the saving node. If both the nodeCollectionName and the nodeName are empty
953      * this is the returned node.
954      * @return the node to which the content is saved
955      * @throws AccessDeniedException
956      * @throws RepositoryException
957      */
958     protected Content getSaveNode(HierarchyManager hm, Content rootNode) throws AccessDeniedException,
959     RepositoryException {
960         Content node = null;
961 
962         // get or create nodeCollection
963         Content nodeCollection = null;
964         if (StringUtils.isNotEmpty(this.getNodeCollectionName())) {
965             try {
966                 nodeCollection = rootNode.getContent(this.getNodeCollectionName());
967             }
968             catch (RepositoryException re) {
969                 // nodeCollection does not exist -> create
970                 nodeCollection = rootNode.createContent(this.getNodeCollectionName(), this.getCollectionNodeCreationItemType());
971                 log.debug("Create - {}" + nodeCollection.getHandle()); //$NON-NLS-1$
972             }
973         }
974         else {
975             nodeCollection = rootNode;
976         }
977 
978         // get or create node
979         if (StringUtils.isNotEmpty(this.getNodeName())) {
980             try {
981                 node = nodeCollection.getContent(this.getNodeName());
982             }
983             catch (RepositoryException re) {
984                 // node does not exist -> create
985                 if (this.getNodeName().equals("mgnlNew")) { //$NON-NLS-1$
986                     this.setNodeName(Path.getUniqueLabel(hm, nodeCollection.getHandle(), "0")); //$NON-NLS-1$
987                 }
988                 node = nodeCollection.createContent(this.getNodeName(), this.getCreationItemType());
989             }
990         }
991         else {
992             node = nodeCollection;
993         }
994         return node;
995     }
996 
997     /**
998      * Saves a uploaded file in the magnolia way. It creates a subnode name_properties where all the information like
999      * the mime type is stored.
1000      * @param node the node under which the data is stored
1001      * @param name the name of the nodedata to store the data into
1002      * @param fileName If empty the original filename is used
1003      * @param template can be empty
1004      * @throws PathNotFoundException
1005      * @throws RepositoryException
1006      * @throws AccessDeniedException
1007      */
1008     public static void saveDocument(Content node, Document doc, String name, String fileName, String template)
1009     throws PathNotFoundException, RepositoryException, AccessDeniedException {
1010 
1011         NodeData data = node.getNodeData(name);
1012         if (doc != null) {
1013             if (!data.isExist()) {
1014                 data = node.createNodeData(name, PropertyType.BINARY);
1015 
1016                 log.debug("creating under - {}", node.getHandle()); //$NON-NLS-1$
1017                 log.debug("creating node data for binary store - {}", name); //$NON-NLS-1$
1018 
1019             }
1020             data.setValue(doc.getStream());
1021             log.debug("Node data updated"); //$NON-NLS-1$
1022         }
1023         if (data != null) {
1024             if (fileName == null || fileName.equals(StringUtils.EMPTY)) {
1025                 fileName = doc.getFileName();
1026             }
1027             data.setAttribute(FileProperties.PROPERTY_FILENAME, fileName);
1028             if (doc != null) {
1029                 data.setAttribute(FileProperties.PROPERTY_CONTENTTYPE, doc.getType());
1030 
1031                 Calendar value = new GregorianCalendar(TimeZone.getDefault());
1032                 data.setAttribute(FileProperties.PROPERTY_LASTMODIFIED, value);
1033 
1034                 data.setAttribute(FileProperties.PROPERTY_SIZE, Long.toString(doc.getLength()));
1035 
1036                 data.setAttribute(FileProperties.PROPERTY_EXTENSION, doc.getExtension());
1037 
1038                 data.setAttribute(FileProperties.PROPERTY_TEMPLATE, template);
1039 
1040                 InputStream raf = null;
1041                 try {
1042                     ImageInfo ii = new ImageInfo();
1043                     raf = new FileInputStream(doc.getFile());
1044                     ii.setInput(raf);
1045                     if (ii.check()) {
1046                         data.setAttribute(FileProperties.PROPERTY_WIDTH, Long.toString(ii.getWidth()));
1047                         data.setAttribute(FileProperties.PROPERTY_HEIGHT, Long.toString(ii.getHeight()));
1048                         // data.setAttribute(FileProperties.x, Long.toString(ii.getBitsPerPixel()));
1049                     }
1050                 }
1051                 catch (FileNotFoundException e) {
1052                     log.error("FileNotFoundException caught when parsing {}, image data will not be available", doc
1053                             .getFile()
1054                             .getAbsolutePath());
1055                 }
1056                 finally {
1057                     IOUtils.closeQuietly(raf);
1058                 }
1059 
1060                 // TODO: check this
1061                 // deleting all the documents in the form AFTER the complete save is done, since some other field save
1062                 // could need the same file.
1063                 // doc.delete();
1064             }
1065         }
1066 
1067     }
1068 
1069     /**
1070      * @see info.magnolia.module.admininterface.SaveHandler#getNodeCollectionName()
1071      */
1072     @Override
1073     public String getNodeCollectionName() {
1074         return this.nodeCollectionName;
1075     }
1076 
1077     /**
1078      * @see info.magnolia.module.admininterface.SaveHandler#setNodeCollectionName(java.lang.String)
1079      */
1080     @Override
1081     public void setNodeCollectionName(String nodeCollectionName) {
1082         this.nodeCollectionName = nodeCollectionName;
1083     }
1084 
1085     /**
1086      * @see info.magnolia.module.admininterface.SaveHandler#getNodeName()
1087      */
1088     @Override
1089     public String getNodeName() {
1090         return this.nodeName;
1091     }
1092 
1093     /**
1094      * @see info.magnolia.module.admininterface.SaveHandler#setNodeName(java.lang.String)
1095      */
1096     @Override
1097     public void setNodeName(String nodeName) {
1098         this.nodeName = nodeName;
1099     }
1100 
1101     /**
1102      * @see info.magnolia.module.admininterface.SaveHandler#getParagraph()
1103      */
1104     @Override
1105     public String getParagraph() {
1106         return this.paragraph;
1107     }
1108 
1109     /**
1110      * @see info.magnolia.module.admininterface.SaveHandler#setParagraph(java.lang.String)
1111      */
1112     @Override
1113     public void setParagraph(String paragraph) {
1114         this.paragraph = paragraph;
1115     }
1116 
1117     /**
1118      * @see info.magnolia.module.admininterface.SaveHandler#getPath()
1119      */
1120     @Override
1121     public String getPath() {
1122         return this.path;
1123     }
1124 
1125     /**
1126      * @see info.magnolia.module.admininterface.SaveHandler#setPath(java.lang.String)
1127      */
1128     @Override
1129     public void setPath(String path) {
1130         this.path = path;
1131     }
1132 
1133 }