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