001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.connector.file;
017
018import static java.nio.file.Files.deleteIfExists;
019
020import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
021
022import org.infinispan.schematic.Schematic;
023import org.infinispan.schematic.document.Document;
024import org.infinispan.schematic.document.EditableDocument;
025import org.infinispan.schematic.document.Json;
026import org.modeshape.jcr.cache.document.DocumentTranslator;
027import org.modeshape.jcr.spi.federation.ExtraPropertiesStore;
028import org.modeshape.jcr.value.Name;
029import org.modeshape.jcr.value.Property;
030
031import java.util.Collections;
032import java.io.File;
033import java.nio.file.Path;
034import java.util.HashMap;
035import java.util.Map;
036import java.io.IOException;
037import java.io.FileInputStream;
038import java.io.FileOutputStream;
039
040/**
041 * An implementation of ExtraPropertyStore, based on
042 * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore that stores the
043 * properties in a separate configured directory than the filesystem federation itself.
044 *
045 * @author Mike Durbin
046 * @author acoburn
047 * @author ajs6f
048 */
049public class ExternalJsonSidecarExtraPropertyStore implements ExtraPropertiesStore {
050
051    private final FedoraFileSystemConnector connector;
052
053    private final File propertyStoreRoot;
054
055    private final DocumentTranslator translator;
056
057    /**
058     * Default constructor.
059     * @param connector the FileSystemConnector for which this class will store properties.
060     * @param propertyStoreRoot the root of a filesystem into which properties will be
061     *                          serialized.
062     */
063    public ExternalJsonSidecarExtraPropertyStore(final FedoraFileSystemConnector connector,
064                                                 final DocumentTranslator translator,
065                                                 final File propertyStoreRoot) {
066        this.connector = connector;
067        this.translator = translator;
068        this.propertyStoreRoot = propertyStoreRoot;
069    }
070
071    protected File sidecarFile(final String id) {
072        final File file;
073        if (connector.isRoot(id)) {
074            file = new File(propertyStoreRoot, "federation-root.modeshape.json");
075        } else {
076            String ext = ".modeshape.json";
077            if (connector.isContentNode(id)) {
078                ext = ".content.modeshape.json";
079            }
080            final File f = new File(connector.fileFor(id).getAbsolutePath() + ext);
081
082            final Path propertyFileInFederation = f.getAbsoluteFile().toPath();
083            final Path relativePath = connector.fileFor("/")
084                            .getAbsoluteFile().toPath().relativize(propertyFileInFederation);
085            file = propertyStoreRoot.toPath().resolve(relativePath).toFile();
086        }
087        if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
088            throw new RepositoryRuntimeException("Unable to create directories " + file.getParentFile() + ".");
089        }
090        return file;
091    }
092
093    /**
094     * This is a trivial reimplementation of the private Modeshape implementation in
095     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
096     *
097     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
098     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L139
099     *
100     * @param id the identifier for the sidecar file
101     * @return whether the file was deleted
102     */
103    @Override
104    public boolean removeProperties(final String id) {
105        try {
106            return deleteIfExists(sidecarFile(id).toPath());
107        } catch (final IOException e) {
108            throw new RepositoryRuntimeException(id, e);
109        }
110    }
111
112
113    /**
114     * This is a trivial reimplementation of the private modeshape implementation in
115     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
116     *
117     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
118     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L60
119     *
120     * @param id the identifier for the sidecar file
121     * @return a map of the properties associated with the given configuration
122     */
123    @Override
124    public Map<Name, Property> getProperties(final String id) {
125        final File sidecarFile = sidecarFile(id);
126        if (!sidecarFile.exists()) {
127            return Collections.emptyMap();
128        }
129        try (final FileInputStream sidecarStream = new FileInputStream(sidecarFile)) {
130            final Document document = Json.read(sidecarStream, false);
131            final Map<Name, Property> results = new HashMap<>();
132            translator.getProperties(document, results);
133            return results;
134        } catch (final IOException e) {
135            throw new RepositoryRuntimeException(id, e);
136        }
137    }
138
139    /**
140     * This is a trivial reimplementation of the private modeshape implementation in
141     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
142     *
143     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
144     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L74
145     *
146     * @param id the id for the sidecar file
147     * @param properties the keys/values to set in the specified sidecar configuration
148     */
149    @Override
150    public void updateProperties(final String id, final Map<Name, Property> properties ) {
151        final File sidecarFile = sidecarFile(id);
152        try {
153            final EditableDocument document;
154            if (!sidecarFile.exists()) {
155                if (properties.isEmpty()) {
156                    return;
157                }
158                sidecarFile.createNewFile();
159                document = Schematic.newDocument();
160            } else {
161                try (final FileInputStream sidecarStream = new FileInputStream(sidecarFile)) {
162                    final Document existing = Json.read(sidecarStream, false);
163                    document = Schematic.newDocument(existing);
164                }
165            }
166            properties.forEach((key, property) -> {
167                if (property == null) {
168                    translator.removeProperty(document, key, null, null);
169                } else {
170                    translator.setProperty(document, property, null, null);
171                }
172            });
173            try (final FileOutputStream outputStream = new FileOutputStream(sidecarFile)) {
174                Json.write(document, outputStream);
175            }
176        } catch (final IOException e) {
177            throw new RepositoryRuntimeException(id, e);
178        }
179    }
180
181    /**
182     * This is a trivial reimplementation of the private modeshape implementation in
183     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
184     *
185     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
186     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L102
187     *
188     * @param id the id for the sidecar file
189     * @param properties the keys/values to set in the specified sidecar configuration
190     */
191    @Override
192    public void storeProperties(final String id, final Map<Name, Property> properties ) {
193        final File sidecarFile = sidecarFile(id);
194        try {
195            if (!sidecarFile.exists()) {
196                if (properties.isEmpty()) {
197                    return;
198                }
199                sidecarFile.createNewFile();
200            }
201            final EditableDocument document = Schematic.newDocument();
202            for (final Property property : properties.values()) {
203                if (property == null) {
204                    continue;
205                }
206                translator.setProperty(document, property, null, null);
207            }
208            try (final FileOutputStream outputStream = new FileOutputStream(sidecarFile)) {
209                Json.write(document, outputStream);
210            }
211        } catch (final IOException e) {
212            throw new RepositoryRuntimeException(id, e);
213        }
214    }
215
216    /**
217     * This is a trivial reimplementation of the private modeshape implementation in
218     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
219     *
220     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
221     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L156
222     *
223     * @param id the id for the sidecar file
224     * @return whether the specified sidecar configuration exists
225     */
226    @Override
227    public boolean contains(final String id) {
228        return sidecarFile(id).exists();
229    }
230
231}