View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.connector.filesystem;
25  
26  import java.io.File;
27  import java.io.FileWriter;
28  import java.io.FilenameFilter;
29  import java.io.IOException;
30  import java.io.Writer;
31  import java.util.ArrayList;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  import org.modeshape.common.text.QuoteEncoder;
42  import org.modeshape.common.text.TextDecoder;
43  import org.modeshape.common.text.TextEncoder;
44  import org.modeshape.common.text.XmlNameEncoder;
45  import org.modeshape.common.util.IoUtil;
46  import org.modeshape.common.util.StringUtil;
47  import org.modeshape.graph.ExecutionContext;
48  import org.modeshape.graph.JcrLexicon;
49  import org.modeshape.graph.Location;
50  import org.modeshape.graph.connector.RepositorySourceException;
51  import org.modeshape.graph.property.Binary;
52  import org.modeshape.graph.property.Name;
53  import org.modeshape.graph.property.Property;
54  import org.modeshape.graph.property.PropertyFactory;
55  import org.modeshape.graph.property.PropertyType;
56  import org.modeshape.graph.property.ValueFactories;
57  import org.modeshape.graph.property.ValueFactory;
58  
59  /**
60   * A {@link CustomPropertiesFactory} implementation that stores "extra" or "custom" properties for 'nt:file', 'nt:folder', and
61   * 'nt:resource' nodes in a separate file that is named the same as the original but with a different extension.
62   */
63  public class StoreProperties extends BasePropertiesFactory {
64  
65      private static final long serialVersionUID = 1L;
66  
67      /**
68       * The regex pattern string used to parse properties. The capture groups are as follows:
69       * <ol>
70       * <li>property name (encoded)</li>
71       * <li>property type string</li>
72       * <li>a '[' if the value is multi-valued</li>
73       * <li>the single value, or comma-separated values</li>
74       * </ol>
75       * <p>
76       * The expression is: <code>([\S]+)\s*[(](\w+)[)]\s*([\[]?)?([^\]]+)[\]]?</code>
77       * </p>
78       */
79      protected static final String PROPERTY_PATTERN_STRING = "([\\S]+)\\s*[(](\\w+)[)]\\s*([\\[]?)?([^\\]]+)[\\]]?";
80      protected static final Pattern PROPERTY_PATTERN = Pattern.compile(PROPERTY_PATTERN_STRING);
81  
82      /**
83       * The regex pattern string used to parse quoted string property values. This is a repeating expression, and group(0) captures
84       * the characters within the quotes (including escaped double quotes).
85       * <p>
86       * The expression is: <code>\"((((?<=\\)\")|[^"])*)\"</code>
87       * </p>
88       */
89      protected static final String STRING_VALUE_PATTERN_STRING = "\\\"((((?<=\\\\)\\\")|[^\"])*)\\\"";
90      protected static final Pattern STRING_VALUE_PATTERN = Pattern.compile(STRING_VALUE_PATTERN_STRING);
91  
92      /**
93       * The regex pattern string used to parse non-string property values (including hexadecimal-encoded binary values). This is a
94       * repeating expression, and group(1) captures the individual values.
95       * <p>
96       * The expression is: <code>([^\s,]+)\s*[,]*\s*</code>
97       * </p>
98       */
99      protected static final String VALUE_PATTERN_STRING = "([^\\s,]+)\\s*[,]*\\s*";
100     protected static final Pattern VALUE_PATTERN = Pattern.compile(VALUE_PATTERN_STRING);
101 
102     public static final String DEFAULT_EXTENSION = ".modeshape";
103     public static final String DEFAULT_RESOURCE_EXTENSION = ".content.modeshape";
104     protected static final Map<Name, Property> NO_PROPERTIES_MAP = Collections.emptyMap();
105 
106     private final String extension;
107     private final String resourceExtension;
108     private String sourceName;
109     private TextEncoder encoder = new XmlNameEncoder();
110     private TextDecoder decoder = new XmlNameEncoder();
111     private QuoteEncoder quoter = new QuoteEncoder();
112 
113     /**
114      * 
115      */
116     public StoreProperties() {
117         extension = DEFAULT_EXTENSION;
118         resourceExtension = DEFAULT_RESOURCE_EXTENSION;
119     }
120 
121     @Override
122     public FilenameFilter getFilenameFilter( final FilenameFilter exclusionFilter ) {
123         final Pattern extensionFilter = Pattern.compile(extension.replaceAll("\\.", "\\\\.") + "$");
124         final Pattern resourceExtensionFilter = Pattern.compile(resourceExtension.replaceAll("\\.", "\\\\.") + "$");
125         return new FilenameFilter() {
126 
127             public boolean accept( File dir,
128                                    String name ) {
129                 if (extensionFilter.matcher(name).matches()) return false;
130                 if (resourceExtensionFilter.matcher(name).matches()) return false;
131                 if (exclusionFilter != null && !exclusionFilter.accept(dir, name)) return false;
132                 return true;
133             }
134         };
135 
136     }
137 
138     /**
139      * @return sourceName
140      */
141     public String getSourceName() {
142         return sourceName;
143     }
144 
145     /**
146      * @param sourceName Sets sourceName to the specified value.
147      */
148     public void setSourceName( String sourceName ) {
149         this.sourceName = sourceName;
150     }
151 
152     /**
153      * {@inheritDoc}
154      * 
155      * @see org.modeshape.connector.filesystem.CustomPropertiesFactory#getDirectoryProperties(org.modeshape.graph.ExecutionContext,
156      *      org.modeshape.graph.Location, java.io.File)
157      */
158     @Override
159     public Collection<Property> getDirectoryProperties( ExecutionContext context,
160                                                         Location location,
161                                                         File directory ) {
162         File parent = directory.getParentFile();
163         if (parent == null) return NO_PROPERTIES_COLLECTION;
164         return load(propertiesFileFor(directory), context).values();
165     }
166 
167     /**
168      * {@inheritDoc}
169      * 
170      * @see org.modeshape.connector.filesystem.CustomPropertiesFactory#getFileProperties(org.modeshape.graph.ExecutionContext,
171      *      org.modeshape.graph.Location, java.io.File)
172      */
173     @Override
174     public Collection<Property> getFileProperties( ExecutionContext context,
175                                                    Location location,
176                                                    File file ) {
177         return load(propertiesFileFor(file), context).values();
178     }
179 
180     /**
181      * {@inheritDoc}
182      * 
183      * @see org.modeshape.connector.filesystem.CustomPropertiesFactory#getResourceProperties(org.modeshape.graph.ExecutionContext,
184      *      org.modeshape.graph.Location, java.io.File, java.lang.String)
185      */
186     @Override
187     public Collection<Property> getResourceProperties( ExecutionContext context,
188                                                        Location location,
189                                                        File file,
190                                                        String mimeType ) {
191         return load(propertiesFileForResource(file), context).values();
192     }
193 
194     /**
195      * {@inheritDoc}
196      * 
197      * @see org.modeshape.connector.filesystem.CustomPropertiesFactory#recordDirectoryProperties(org.modeshape.graph.ExecutionContext,
198      *      java.lang.String, org.modeshape.graph.Location, java.io.File, java.util.Map)
199      */
200     @Override
201     public Set<Name> recordDirectoryProperties( ExecutionContext context,
202                                                 String sourceName,
203                                                 Location location,
204                                                 File file,
205                                                 Map<Name, Property> properties ) throws RepositorySourceException {
206         return write(propertiesFileFor(file), context, properties);
207     }
208 
209     /**
210      * {@inheritDoc}
211      * 
212      * @see org.modeshape.connector.filesystem.CustomPropertiesFactory#recordFileProperties(org.modeshape.graph.ExecutionContext,
213      *      java.lang.String, org.modeshape.graph.Location, java.io.File, java.util.Map)
214      */
215     @Override
216     public Set<Name> recordFileProperties( ExecutionContext context,
217                                            String sourceName,
218                                            Location location,
219                                            File file,
220                                            Map<Name, Property> properties ) throws RepositorySourceException {
221         return write(propertiesFileFor(file), context, properties);
222     }
223 
224     /**
225      * {@inheritDoc}
226      * 
227      * @see org.modeshape.connector.filesystem.CustomPropertiesFactory#recordResourceProperties(org.modeshape.graph.ExecutionContext,
228      *      java.lang.String, org.modeshape.graph.Location, java.io.File, java.util.Map)
229      */
230     @Override
231     public Set<Name> recordResourceProperties( ExecutionContext context,
232                                                String sourceName,
233                                                Location location,
234                                                File file,
235                                                Map<Name, Property> properties ) throws RepositorySourceException {
236         return write(propertiesFileForResource(file), context, properties);
237     }
238 
239     protected File propertiesFileFor( File fileOrDirectory ) {
240         return new File(fileOrDirectory.getPath() + extension);
241     }
242 
243     protected File propertiesFileForResource( File fileOrDirectory ) {
244         return new File(fileOrDirectory.getPath() + resourceExtension);
245     }
246 
247     protected Map<Name, Property> load( File propertiesFile,
248                                         ExecutionContext context ) throws RepositorySourceException {
249         if (!propertiesFile.exists() || !propertiesFile.canRead()) return NO_PROPERTIES_MAP;
250         try {
251             String content = IoUtil.read(propertiesFile);
252             ValueFactories factories = context.getValueFactories();
253             PropertyFactory propFactory = context.getPropertyFactory();
254             Map<Name, Property> result = new HashMap<Name, Property>();
255             for (String line : StringUtil.splitLines(content)) {
256                 // Parse each line ...
257                 Property property = parse(line, factories, propFactory);
258                 if (property != null) {
259                     result.put(property.getName(), property);
260                 }
261             }
262             return result;
263         } catch (IOException e) {
264             throw new RepositorySourceException(sourceName, e);
265         }
266     }
267 
268     protected Set<Name> write( File propertiesFile,
269                                ExecutionContext context,
270                                Map<Name, Property> properties ) throws RepositorySourceException {
271         if (properties.isEmpty()) {
272             if (propertiesFile.exists()) {
273                 // Delete the file ...
274                 propertiesFile.delete();
275             }
276             return Collections.emptySet();
277         }
278         Set<Name> names = new HashSet<Name>();
279         try {
280             ValueFactory<String> strings = context.getValueFactories().getStringFactory();
281             Writer fileWriter = new FileWriter(propertiesFile);
282             try {
283                 // Write the primary type first ...
284                 Property primaryType = properties.get(JcrLexicon.PRIMARY_TYPE);
285                 if (primaryType != null) {
286                     write(primaryType, fileWriter, strings);
287                     names.add(primaryType.getName());
288                 }
289                 // Then write the mixin types ...
290                 Property mixinTypes = properties.get(JcrLexicon.MIXIN_TYPES);
291                 if (mixinTypes != null) {
292                     write(mixinTypes, fileWriter, strings);
293                     names.add(mixinTypes.getName());
294                 }
295                 // Then write the UUID ...
296                 Property uuid = properties.get(JcrLexicon.UUID);
297                 if (uuid != null) {
298                     write(uuid, fileWriter, strings);
299                     names.add(uuid.getName());
300                 }
301                 // Then all the others ...
302                 for (Property property : properties.values()) {
303                     if (property == primaryType || property == mixinTypes || property == uuid) continue;
304                     write(property, fileWriter, strings);
305                     names.add(property.getName());
306                 }
307             } finally {
308                 fileWriter.close();
309             }
310         } catch (IOException e) {
311             throw new RepositorySourceException(sourceName, e);
312         }
313         return names;
314     }
315 
316     protected void write( Property property,
317                           Writer stream,
318                           ValueFactory<String> strings ) throws IOException {
319         String name = strings.create(property.getName());
320         stream.append(encoder.encode(name));
321         if (property.isEmpty()) return;
322         stream.append(" (");
323         PropertyType type = PropertyType.discoverType(property.getFirstValue());
324         stream.append(type.getName().toLowerCase());
325         stream.append(") ");
326         if (property.isMultiple()) {
327             stream.append('[');
328         }
329         boolean first = true;
330         boolean quote = type == PropertyType.STRING;
331         for (Object value : property) {
332             if (first) first = false;
333             else stream.append(", ");
334             String str = null;
335             if (value instanceof Binary) {
336                 str = StringUtil.getHexString(((Binary)value).getBytes());
337             } else {
338                 str = strings.create(value);
339             }
340             if (quote) {
341                 stream.append('"');
342                 stream.append(quoter.encode(str));
343                 stream.append('"');
344             } else {
345                 stream.append(str);
346             }
347         }
348         if (property.isMultiple()) {
349             stream.append(']');
350         }
351         stream.append('\n');
352         stream.flush();
353     }
354 
355     protected Property parse( String line,
356                               ValueFactories factories,
357                               PropertyFactory propFactory ) {
358         if (line.length() == 0) return null; // blank line
359         char firstChar = line.charAt(0);
360         if (firstChar == '#') return null; // comment line
361         if (firstChar == ' ') return null; // ignore line
362         Matcher matcher = PROPERTY_PATTERN.matcher(line);
363         if (!matcher.matches()) {
364             // It should be an empty multi-valued property, and the line consists only of the name ...
365             Name name = factories.getNameFactory().create(decoder.decode(line));
366             return propFactory.create(name);
367         }
368 
369         String nameString = decoder.decode(matcher.group(1));
370         String typeString = matcher.group(2);
371         String valuesString = matcher.group(4);
372 
373         Name name = factories.getNameFactory().create(nameString);
374         PropertyType type = PropertyType.valueFor(typeString);
375 
376         Pattern pattern = VALUE_PATTERN;
377         ValueFactory<?> valueFactory = factories.getValueFactory(type);
378         boolean binary = false;
379         boolean decode = false;
380         if (type == PropertyType.STRING) {
381             // Parse the double-quoted value(s) ...
382             pattern = STRING_VALUE_PATTERN;
383             decode = true;
384         } else if (type == PropertyType.BINARY) {
385             binary = true;
386         }
387         Matcher valuesMatcher = pattern.matcher(valuesString);
388         List<Object> values = new ArrayList<Object>();
389         while (valuesMatcher.find()) {
390             String valueString = valuesMatcher.group(1);
391             if (binary) {
392                 // The value is a hexadecimal-encoded byte array ...
393                 byte[] binaryValue = StringUtil.fromHexString(valueString);
394                 Object value = valueFactory.create(binaryValue);
395                 values.add(value);
396             } else {
397                 if (decode) valueString = quoter.decode(valueString);
398                 Object value = valueFactory.create(valueString);
399                 values.add(value);
400             }
401         }
402         if (values.isEmpty()) return null;
403         return propFactory.create(name, type, values);
404     }
405 }