/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.connector.filesystem; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.jcr.NamespaceRegistry; import javax.jcr.RepositoryException; import org.modeshape.common.text.QuoteEncoder; import org.modeshape.common.text.TextDecoder; import org.modeshape.common.text.TextEncoder; import org.modeshape.common.text.XmlNameEncoder; import org.modeshape.common.util.IoUtil; import org.modeshape.common.util.StringUtil; import org.modeshape.jcr.JcrLexicon; import org.modeshape.jcr.api.Binary; import org.modeshape.jcr.cache.DocumentStoreException; import org.modeshape.jcr.spi.federation.ExtraPropertiesStore; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.NameFactory; import org.modeshape.jcr.value.Property; import org.modeshape.jcr.value.PropertyFactory; import org.modeshape.jcr.value.PropertyType; import org.modeshape.jcr.value.ValueFactories; import org.modeshape.jcr.value.ValueFactory; import org.modeshape.jcr.value.ValueFormatException; /** * An {@link ExtraPropertiesStore} implementation that stores extra properties in legacy sidecar files adjacent to the actual file * or directory corresponding to the external node. The format of these legacy files is compatible with thosed used by the * ModeShape 2.x file system connector. */ class LegacySidecarExtraPropertyStore implements ExtraPropertiesStore { /** * The regex pattern string used to parse properties. The capture groups are as follows: * <ol> * <li>property name (encoded)</li> * <li>property type string</li> * <li>a '[' if the value is multi-valued</li> * <li>the single value, or comma-separated values</li> * </ol> * <p> * The expression is: <code>([\S]+)\s*[(](\w+)[)]\s*([\[]?)?([^\]]+)[\]]?</code> * </p> */ protected static final String PROPERTY_PATTERN_STRING = "([\\S]+)\\s*[(](\\w+)[)]\\s*([\\[]?)?([^\\]]+)[\\]]?"; protected static final Pattern PROPERTY_PATTERN = Pattern.compile(PROPERTY_PATTERN_STRING); /** * The regex pattern string used to parse quoted string property values. This is a repeating expression, and group(0) captures * the characters within the quotes (including escaped double quotes). * <p> * The expression is: <code>\"((((?<=\\)\")|[^"])*)\"</code> * </p> */ protected static final String STRING_VALUE_PATTERN_STRING = "\\\"((((?<=\\\\)\\\")|[^\"])*)\\\""; protected static final Pattern STRING_VALUE_PATTERN = Pattern.compile(STRING_VALUE_PATTERN_STRING); /** * The regex pattern string used to parse non-string property values (including hexadecimal-encoded binary values). This is a * repeating expression, and group(1) captures the individual values. * <p> * The expression is: <code>([^\s,]+)\s*[,]*\s*</code> * </p> */ protected static final String VALUE_PATTERN_STRING = "([^\\s,]+)\\s*[,]*\\s*"; protected static final Pattern VALUE_PATTERN = Pattern.compile(VALUE_PATTERN_STRING); public static final String DEFAULT_EXTENSION = ".modeshape"; public static final String DEFAULT_RESOURCE_EXTENSION = ".content.modeshape"; private final FileSystemConnector connector; private final NamespaceRegistry registry; private final PropertyFactory propertyFactory; private final ValueFactories factories; private final ValueFactory<String> stringFactory; private final TextEncoder encoder = new XmlNameEncoder(); private final TextDecoder decoder = new XmlNameEncoder(); private final QuoteEncoder quoter = new QuoteEncoder(); protected LegacySidecarExtraPropertyStore( FileSystemConnector connector ) { this.connector = connector; this.registry = this.connector.registry(); this.propertyFactory = this.connector.getContext().getPropertyFactory(); this.factories = this.connector.getContext().getValueFactories(); this.stringFactory = factories.getStringFactory(); } protected String getExclusionPattern() { return "(.+)\\.(content\\.)?modeshape$"; } @Override public Map<Name, Property> getProperties( String id ) { return load(id, sidecarFile(id)); } @Override public void storeProperties( String id, Map<Name, Property> properties ) { write(id, sidecarFile(id), properties); } @Override public void updateProperties( String id, Map<Name, Property> properties ) { File sidecar = sidecarFile(id); Map<Name, Property> existing = load(id, sidecar); if (existing == null || existing.isEmpty()) { write(id, sidecar, properties); } else { // Merge the changed properties into the existing ... for (Map.Entry<Name, Property> entry : properties.entrySet()) { Name name = entry.getKey(); Property property = entry.getValue(); if (property == null) { existing.remove(name); } else { existing.put(name, property); } } // Now write out all of the updated properties ... write(id, sidecar, existing); } } @Override public boolean removeProperties( String id ) { File file = sidecarFile(id); if (!file.exists()) return false; file.delete(); return true; } protected File sidecarFile( String id ) { File actualFile = connector.fileFor(id); String extension = DEFAULT_EXTENSION; if (connector.isContentNode(id)) { extension = DEFAULT_RESOURCE_EXTENSION; } return new File(actualFile.getAbsolutePath() + extension); } protected Map<Name, Property> load( String id, File propertiesFile ) { if (!propertiesFile.exists() || !propertiesFile.canRead()) return NO_PROPERTIES; try { String content = IoUtil.read(propertiesFile); Map<Name, Property> result = new HashMap<Name, Property>(); for (String line : StringUtil.splitLines(content)) { // Parse each line ... Property property = parse(line, result); if (property != null) { result.put(property.getName(), property); } } return result; } catch (Throwable e) { throw new DocumentStoreException(id, e); } } protected void write( String id, File propertiesFile, Map<Name, Property> properties ) { if (properties.isEmpty()) { if (propertiesFile.exists()) { // Delete the file ... propertiesFile.delete(); } return; } try { Writer fileWriter = null; try { // Write the primary type first ... Property primaryType = properties.get(JcrLexicon.PRIMARY_TYPE); if (primaryType != null) { fileWriter = new FileWriter(propertiesFile); write(primaryType, fileWriter); } // Then write the mixin types ... Property mixinTypes = properties.get(JcrLexicon.MIXIN_TYPES); if (mixinTypes != null) { if (fileWriter == null) fileWriter = new FileWriter(propertiesFile); write(mixinTypes, fileWriter); } // Then write the UUID ... Property uuid = properties.get(JcrLexicon.UUID); if (uuid != null) { if (fileWriter == null) fileWriter = new FileWriter(propertiesFile); write(uuid, fileWriter); } // Then all the others ... for (Property property : properties.values()) { if (property == null) continue; if (property == primaryType || property == mixinTypes || property == uuid) continue; if (fileWriter == null) fileWriter = new FileWriter(propertiesFile); write(property, fileWriter); } } finally { if (fileWriter != null) { fileWriter.close(); } else { // Nothing was written, so remove the sidecar file ... propertiesFile.delete(); } } } catch (Throwable e) { throw new DocumentStoreException(id, e); } } protected void write( Property property, Writer stream ) throws IOException, RepositoryException { String name = stringFactory.create(property.getName()); stream.append(encoder.encode(name)); if (property.isEmpty()) { stream.append('\n'); stream.flush(); return; } stream.append(" ("); PropertyType type = PropertyType.discoverType(property.getFirstValue()); stream.append(type.getName().toLowerCase()); stream.append(") "); if (property.isMultiple()) { stream.append('['); } boolean first = true; boolean quote = type == PropertyType.STRING; for (Object value : property) { if (first) first = false; else stream.append(", "); String str = null; if (value instanceof Binary) { byte[] bytes = IoUtil.readBytes(((Binary)value).getStream()); str = StringUtil.getHexString(bytes); } else { str = stringFactory.create(value); } if (quote) { stream.append('"'); stream.append(quoter.encode(str)); stream.append('"'); } else { stream.append(str); } } if (property.isMultiple()) { stream.append(']'); } stream.append('\n'); stream.flush(); } protected Property parse( String line, Map<Name, Property> result ) throws RepositoryException { if (line.length() == 0) return null; // blank line char firstChar = line.charAt(0); if (firstChar == '#') return null; // comment line if (firstChar == ' ') return null; // ignore line Matcher matcher = PROPERTY_PATTERN.matcher(line); NameFactory nameFactory = factories.getNameFactory(); if (!matcher.matches()) { // It should be an empty multi-valued property, and the line consists only of the name ... Name name = nameFactory.create(decoder.decode(line)); return propertyFactory.create(name); } String nameString = decoder.decode(matcher.group(1)); String typeString = matcher.group(2); String valuesString = matcher.group(4); Name name = null; try { name = factories.getNameFactory().create(nameString); } catch (ValueFormatException e) { // See MODE-1281. Earlier versions would write out an empty property without the trailing line feed, // so we need to consider this case now. About the only thing we can do is look for two namespace-prefixed names // ... if (nameString.indexOf(':') < nameString.lastIndexOf(':')) { // This is likely two names smashed together. Use the namespace prefixes to look for where we can break this // ... Set<String> prefixes = new LinkedHashSet<String>(); prefixes.add(JcrLexicon.Namespace.PREFIX); for (String prefix : registry.getPrefixes()) { prefixes.add(prefix); } for (String prefix : prefixes) { int index = nameString.lastIndexOf(prefix + ":"); if (index <= 0) continue; // Otherwise, we found a match. Parse the first property name, and create an empty property ... name = nameFactory.create(nameString.substring(0, index)); result.put(name, propertyFactory.create(name)); // Now parse the name of the next property and continue ... name = nameFactory.create(nameString.substring(index)); break; } } else { throw e; } } PropertyType type = PropertyType.valueFor(typeString); Pattern pattern = VALUE_PATTERN; ValueFactory<?> valueFactory = factories.getValueFactory(type); boolean binary = false; boolean decode = false; if (type == PropertyType.STRING) { // Parse the double-quoted value(s) ... pattern = STRING_VALUE_PATTERN; decode = true; } else if (type == PropertyType.BINARY) { binary = true; } Matcher valuesMatcher = pattern.matcher(valuesString); List<Object> values = new ArrayList<Object>(); while (valuesMatcher.find()) { String valueString = valuesMatcher.group(1); if (binary) { // The value is a hexadecimal-encoded byte array ... byte[] binaryValue = StringUtil.fromHexString(valueString); Object value = valueFactory.create(binaryValue); values.add(value); } else { if (decode) valueString = quoter.decode(valueString); Object value = valueFactory.create(valueString); values.add(value); } } if (values.isEmpty()) return null; return propertyFactory.create(name, type, values.size() > 1 ? values : values.get(0)); } @Override public boolean contains( String id ) { File file = sidecarFile(id); return file.exists(); } }