View Javadoc
1   /**
2    * This file Copyright (c) 2017-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.jcr.util;
35  
36  import info.magnolia.cms.util.SelectorUtil;
37  import info.magnolia.init.MagnoliaConfigurationProperties;
38  
39  import java.nio.charset.StandardCharsets;
40  
41  import javax.inject.Inject;
42  import javax.inject.Singleton;
43  import javax.jcr.Node;
44  import javax.jcr.RepositoryException;
45  import javax.jcr.Session;
46  
47  import org.apache.commons.io.FilenameUtils;
48  import org.apache.commons.lang3.StringUtils;
49  
50  /**
51   * Util component for creation of an unique and validated node name.
52   */
53  @Singleton
54  public class NodeNameHelper {
55  
56      private final String DEFAULT_UNTITLED_NODE_NAME = "untitled";
57  
58      private final MagnoliaConfigurationProperties magnoliaConfigurationProperties;
59  
60      @Inject
61      public NodeNameHelper(MagnoliaConfigurationProperties magnoliaConfigurationProperties) {
62          this.magnoliaConfigurationProperties = magnoliaConfigurationProperties;
63      }
64  
65      /**
66       * Checks if an item already exists with given name in JCR, and returns a unique name;
67       * if necessary, an increment is appended to the name.
68       *
69       * <p>Given the following tree:</p>
70       * <pre>
71       * /
72       * ├── a
73       * ├── b
74       * └── b0
75       *
76       * NodeUtil.getUniqueName(session, "/", "a")  = "a0"
77       * NodeUtil.getUniqueName(session, "/", "b")  = "b1"
78       * NodeUtil.getUniqueName(session, "/", "b0") = "b1"
79       * NodeUtil.getUniqueName(session, "/", "c")  = "c"
80       * </pre>
81       */
82      public String getUniqueName(Session session, String parentPath, String name) throws RepositoryException {
83          if (parentPath.equals("/")) {
84              parentPath = StringUtils.EMPTY;
85          }
86          while (session.itemExists(parentPath + "/" + name)) {
87              name = createUniqueName(name);
88          }
89          return name;
90      }
91  
92      /**
93       * Checks if an item already exists with given name in JCR, and returns a unique name;
94       * if necessary, an increment is inserted into the name, before the given extension.
95       *
96       * <p>Given the following tree:</p>
97       * <pre>
98       * /
99       * ├── a.txt
100      * ├── b.txt
101      * └── b0.txt
102      *
103      * NodeUtil.getUniqueName(session, "/", "a.txt", "txt")  = "a0.txt"
104      * NodeUtil.getUniqueName(session, "/", "b.txt", "txt")  = "b1.txt"
105      * NodeUtil.getUniqueName(session, "/", "b0.txt", "txt") = "b1.txt"
106      * NodeUtil.getUniqueName(session, "/", "c.txt", "txt")  = "c.txt"
107      * NodeUtil.getUniqueName(session, "/", "a.foo", "txt")  = "a.foo"
108      * </pre>
109      */
110     public String getUniqueName(Session session, String parentPath, String name, String extension) throws RepositoryException {
111         if (StringUtils.isNotEmpty(extension) && extension.equals(FilenameUtils.getExtension(name))) {
112             parentPath = "/".equals(parentPath) ? StringUtils.EMPTY : parentPath;
113             String basename = FilenameUtils.getBaseName(name);
114             String fullName = name;
115 
116             while (session.itemExists(parentPath + "/" + fullName)) {
117                 basename = createUniqueName(basename);
118                 fullName = basename + FilenameUtils.EXTENSION_SEPARATOR + extension;
119             }
120             return fullName;
121         }
122 
123         return getUniqueName(session, parentPath, name);
124     }
125 
126     public String getUniqueName(Node parent, String name) throws RepositoryException {
127         while (parent.hasNode(name) || parent.hasProperty(name)) {
128             name = createUniqueName(name);
129         }
130         return name;
131     }
132 
133     /**
134      * Replace illegal characters based on magnolia property magnolia.ut8.enabled.
135      *
136      * @return validated label
137      */
138     public String getValidatedName(String name) {
139         String charset = StringUtils.EMPTY;
140         if (magnoliaConfigurationProperties.getBooleanProperty(MagnoliaConfigurationProperties.MAGNOLIA_UTF8_ENABLED)) {
141             charset = StandardCharsets.UTF_8.name();
142         }
143         return getValidatedName(name, charset);
144     }
145 
146     /**
147      * If charset equals <code>UTF-8</code>, replaces the following characters with a dash <code>-</code> :
148      * <p>
149      * Jackrabbit not allowed {@code 32: [ ] 91: [[] 93: []] 42: [*] 34: ["] 58 [:] 92: [\] 39 :[']}
150      * <p>
151      * URL not valid {@code 59: [;] 47: [/] 63: [?] 43: [+] 37: [%] 33: [!] 35:[#] 94: [^]}.
152      * <p>
153      * Otherwise, replaces illegal characters with a dash <code>-</code> except for {@code [_] [0-9], [A-Z], [a-z], [-], [_], [.]}.
154      * <p>
155      * Please notice that a valid name can not begin with dot or period <code>[.]</code>.
156      *
157      * @return a validated name for a node.
158      */
159     public String getValidatedName(String name, String charset) {
160         if (StringUtils.isEmpty(name)) {
161             return DEFAULT_UNTITLED_NODE_NAME;
162         }
163         final StringBuilder newLabel = new StringBuilder(name.length());
164 
165         // name cannot begin with . (dot)
166         int ch = name.charAt(0);
167         if (!isCharValid(ch, charset) || ch == 46) {
168             newLabel.append("-");
169         } else {
170             newLabel.append(name.charAt(0));
171         }
172 
173         for (int i = 1; i < name.length(); i++) {
174             int charCode = name.charAt(i);
175             if (isCharValid(charCode, charset)) {
176                 newLabel.append(name.charAt(i));
177             } else {
178                 newLabel.append("-");
179             }
180         }
181         if (newLabel.length() == 0) {
182             newLabel.append(DEFAULT_UNTITLED_NODE_NAME);
183         }
184         return newLabel.toString();
185     }
186 
187     public boolean isValidName(String name) {
188         String charset = StringUtils.EMPTY;
189         if (magnoliaConfigurationProperties.getBooleanProperty(MagnoliaConfigurationProperties.MAGNOLIA_UTF8_ENABLED)) {
190             charset = StandardCharsets.UTF_8.name();
191         }
192         return isValidName(name, charset);
193     }
194 
195     public boolean isValidName(String name, String charset) {
196         return name.equals(getValidatedName(name, charset));
197     }
198 
199     private boolean isCharValid(int charCode, String charset) {
200         // TODO fgrilli: we now allow dots (.) in JR local names but actually in JR 2.0 other chars could be allowed as well
201         // (see http://www.day.com/specs/jcr/2.0/3_Repository_Model.html paragraph 2.2 and org.apache.jackrabbit.util.XMLChar.isValid()).
202         // Also, now that we're on java 6 and JR 2.0 should the check for the charset be dropped?
203 
204         // http://www.ietf.org/rfc/rfc1738.txt
205         // safe = "$" | "-" | "_" | "." | "+"
206         // extra = "!" | "*" | "'" | "(" | ")" | ","
207         // national = "{" | "}" | "|" | "\" | "^" | "~" | "[" | "]" | "`"
208         // punctuation = "<" | ">" | "#" | "%" | <">
209         // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "="
210 
211         if (StandardCharsets.UTF_8.name().equals(charset)) {
212             // jackrabbit not allowed 32: [ ] 91: [[] 93: []] 42: [*] 34: ["] 46: [.] 58 [:] 92: [\] 39 :[']
213             // url not valid 59: [;] 47: [/] 63: [?] 43: [+] 37: [%] 33: [!] 35:[#]
214             if (charCode != 32
215                     && charCode != '['
216                     && charCode != ']'
217                     && charCode != '*'
218                     && charCode != '"'
219                     && charCode != ':'
220                     && charCode != 92
221                     && charCode != 39
222                     && charCode != ';'
223                     && charCode != '/'
224                     && charCode != '?'
225                     && charCode != '+'
226                     && charCode != '%'
227                     && charCode != '!'
228                     && charCode != '#'
229                     && charCode != '@'
230                     && charCode != '='
231                     && charCode != '&'
232                     && charCode != SelectorUtil.SELECTOR_DELIMITER.charAt(0)) {
233                 return true;
234             }
235         } else {
236             // charCodes: 48-57: [0-9]; 65-90: [A-Z]; 97-122: [a-z]; 45: [-]; 95:[_]
237             if (((charCode >= 48) && (charCode <= 57))
238                     || ((charCode >= 65) && (charCode <= 90))
239                     || ((charCode >= 97) && (charCode <= 122))
240                     || charCode == 45
241                     || charCode == 46
242                     || charCode == 95) {
243                 return true;
244             }
245 
246         }
247         return false;
248     }
249 
250     private String createUniqueName(String baseName) {
251         int pos;
252         for (pos = baseName.length() - 1; pos >= 0; pos--) {
253             char c = baseName.charAt(pos);
254             if (c < '0' || c > '9') {
255                 break;
256             }
257         }
258         String base;
259         int cnt;
260         if (pos == -1) {
261             if (baseName.length() > 1) {
262                 pos = baseName.length() - 2;
263             }
264         }
265         if (pos == -1) {
266             base = baseName;
267             cnt = -1;
268         } else {
269             pos++;
270             base = baseName.substring(0, pos);
271             if (pos == baseName.length()) {
272                 cnt = -1;
273             } else {
274                 cnt = Integer.parseInt(baseName.substring(pos));
275             }
276         }
277         return (base + ++cnt);
278     }
279 }