All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.modeshape.connector.filesystem.LegacySidecarExtraPropertyStore Maven / Gradle / Ivy

There is a newer version: 5.4.1.Final
Show newest version
/*
 * 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:
     * 
    *
  1. property name (encoded)
  2. *
  3. property type string
  4. *
  5. a '[' if the value is multi-valued
  6. *
  7. the single value, or comma-separated values
  8. *
*

* The expression is: ([\S]+)\s*[(](\w+)[)]\s*([\[]?)?([^\]]+)[\]]? *

*/ 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). *

* The expression is: \"((((?<=\\)\")|[^"])*)\" *

*/ 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. *

* The expression is: ([^\s,]+)\s*[,]*\s* *

*/ 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 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 getProperties( String id ) { return load(id, sidecarFile(id)); } @Override public void storeProperties( String id, Map properties ) { write(id, sidecarFile(id), properties); } @Override public void updateProperties( String id, Map properties ) { File sidecar = sidecarFile(id); Map existing = load(id, sidecar); if (existing == null || existing.isEmpty()) { write(id, sidecar, properties); } else { // Merge the changed properties into the existing ... for (Map.Entry 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 load( String id, File propertiesFile ) { if (!propertiesFile.exists() || !propertiesFile.canRead()) return NO_PROPERTIES; try { String content = IoUtil.read(propertiesFile); Map result = new HashMap(); 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 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 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 prefixes = new LinkedHashSet(); 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 values = new ArrayList(); 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); } @Override public boolean contains( String id ) { File file = sidecarFile(id); return file.exists(); } }