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 translator the utility to translate properties to/from the JSON configuration
061     * @param propertyStoreRoot the root of a filesystem into which properties will be
062     *                          serialized.
063     */
064    public ExternalJsonSidecarExtraPropertyStore(final FedoraFileSystemConnector connector,
065                                                 final DocumentTranslator translator,
066                                                 final File propertyStoreRoot) {
067        this.connector = connector;
068        this.translator = translator;
069        this.propertyStoreRoot = propertyStoreRoot;
070    }
071
072    protected File sidecarFile(final String id) {
073        final File file;
074        if (connector.isRoot(id)) {
075            file = new File(propertyStoreRoot, "federation-root.modeshape.json");
076        } else {
077            String ext = ".modeshape.json";
078            if (connector.isContentNode(id)) {
079                ext = ".content.modeshape.json";
080            }
081            final File f = new File(connector.fileFor(id).getAbsolutePath() + ext);
082
083            final Path propertyFileInFederation = f.getAbsoluteFile().toPath();
084            final Path relativePath = connector.fileFor("/")
085                            .getAbsoluteFile().toPath().relativize(propertyFileInFederation);
086            file = propertyStoreRoot.toPath().resolve(relativePath).toFile();
087        }
088        if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
089            throw new RepositoryRuntimeException("Unable to create directories " + file.getParentFile() + ".");
090        }
091        return file;
092    }
093
094    /**
095     * This is a trivial reimplementation of the private Modeshape implementation in
096     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
097     *
098     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
099     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L139
100     *
101     * @param id the identifier for the sidecar file
102     * @return whether the file was deleted
103     */
104    @Override
105    public boolean removeProperties(final String id) {
106        try {
107            return deleteIfExists(sidecarFile(id).toPath());
108        } catch (final IOException e) {
109            throw new RepositoryRuntimeException(id, e);
110        }
111    }
112
113
114    /**
115     * This is a trivial reimplementation of the private modeshape implementation in
116     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
117     *
118     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
119     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L60
120     *
121     * @param id the identifier for the sidecar file
122     * @return a map of the properties associated with the given configuration
123     */
124    @Override
125    public Map<Name, Property> getProperties(final String id) {
126        final File sidecarFile = sidecarFile(id);
127        if (!sidecarFile.exists()) {
128            return Collections.emptyMap();
129        }
130        try (final FileInputStream sidecarStream = new FileInputStream(sidecarFile)) {
131            final Document document = Json.read(sidecarStream, false);
132            final Map<Name, Property> results = new HashMap<>();
133            translator.getProperties(document, results);
134            return results;
135        } catch (final IOException e) {
136            throw new RepositoryRuntimeException(id, e);
137        }
138    }
139
140    /**
141     * This is a trivial reimplementation of the private modeshape implementation in
142     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
143     *
144     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
145     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L74
146     *
147     * @param id the id for the sidecar file
148     * @param properties the keys/values to set in the specified sidecar configuration
149     */
150    @Override
151    public void updateProperties(final String id, final Map<Name, Property> properties ) {
152        final File sidecarFile = sidecarFile(id);
153        try {
154            final EditableDocument document;
155            if (!sidecarFile.exists()) {
156                if (properties.isEmpty()) {
157                    return;
158                }
159                sidecarFile.createNewFile();
160                document = Schematic.newDocument();
161            } else {
162                try (final FileInputStream sidecarStream = new FileInputStream(sidecarFile)) {
163                    final Document existing = Json.read(sidecarStream, false);
164                    document = Schematic.newDocument(existing);
165                }
166            }
167            properties.forEach((key, property) -> {
168                if (property == null) {
169                    translator.removeProperty(document, key, null, null);
170                } else {
171                    translator.setProperty(document, property, null, null);
172                }
173            });
174            try (final FileOutputStream outputStream = new FileOutputStream(sidecarFile)) {
175                Json.write(document, outputStream);
176            }
177        } catch (final IOException e) {
178            throw new RepositoryRuntimeException(id, e);
179        }
180    }
181
182    /**
183     * This is a trivial reimplementation of the private modeshape implementation in
184     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
185     *
186     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
187     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L102
188     *
189     * @param id the id for the sidecar file
190     * @param properties the keys/values to set in the specified sidecar configuration
191     */
192    @Override
193    public void storeProperties(final String id, final Map<Name, Property> properties ) {
194        final File sidecarFile = sidecarFile(id);
195        try {
196            if (!sidecarFile.exists()) {
197                if (properties.isEmpty()) {
198                    return;
199                }
200                sidecarFile.createNewFile();
201            }
202            final EditableDocument document = Schematic.newDocument();
203            for (final Property property : properties.values()) {
204                if (property == null) {
205                    continue;
206                }
207                translator.setProperty(document, property, null, null);
208            }
209            try (final FileOutputStream outputStream = new FileOutputStream(sidecarFile)) {
210                Json.write(document, outputStream);
211            }
212        } catch (final IOException e) {
213            throw new RepositoryRuntimeException(id, e);
214        }
215    }
216
217    /**
218     * This is a trivial reimplementation of the private modeshape implementation in
219     * org.modeshape.connector.filesystem.JsonSidecarExtraPropertyStore
220     *
221     * See: https://github.com/ModeShape/modeshape/blob/modeshape-4.2.0.Final/modeshape-jcr/src/main/java/
222     *              org/modeshape/connector/filesystem/JsonSidecarExtraPropertyStore.java#L156
223     *
224     * @param id the id for the sidecar file
225     * @return whether the specified sidecar configuration exists
226     */
227    @Override
228    public boolean contains(final String id) {
229        return sidecarFile(id).exists();
230    }
231
232}