package io.eguan.vvr.persistence.repository; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * 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. * #L% */ import static io.eguan.vvr.remote.VvrRemoteUtils.newUuid; import io.eguan.nrs.NrsException; import io.eguan.nrs.NrsFile; import io.eguan.nrs.NrsFileFlag; import io.eguan.nrs.NrsFileHeader; import io.eguan.nrs.NrsFileJanitor; import io.eguan.proto.Common.OpCode; import io.eguan.proto.Common.Type; import io.eguan.proto.vvr.VvrRemote.Item; import io.eguan.proto.vvr.VvrRemote.RemoteOperation; import io.eguan.utils.Files; import io.eguan.utils.UuidT; import io.eguan.vvr.remote.VvrRemoteUtils; import io.eguan.vvr.repository.core.api.AbstractUniqueVvrObject; import io.eguan.vvr.repository.core.api.Device; import io.eguan.vvr.repository.core.api.FutureVoid; import io.eguan.vvr.repository.core.api.Snapshot; import io.eguan.vvr.repository.core.api.VvrItem; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.UUID; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.MoreObjects; /** * @author oodrive * @author llambert * @author pwehrle * @author jmcaba * @author ebredzinski */ abstract class NrsVvrItem extends AbstractUniqueVvrObject implements VvrItem { static final String UUID_KEY = "uuid"; // TODO should all be private static final String NAME_KEY = "name"; static final String DESC_KEY = "desc"; private static final Logger LOGGER = LoggerFactory.getLogger(NrsVvrItem.class); /** * The underlying VVR. */ private final NrsRepository vvr; /** Path of the persistence file. */ private final File persistenceFile; /** Type of object in remote messages */ private final Type msgObjectType; /** The {@link NrsFile} backing this instance. */ private NrsFile nrsFile; /** The UUID of the 'existing' parent snapshot. Lazy init */ private volatile UUID parentUuid; /** * Internal builder constructor to be invoked by subclass builders. * * @param builder * the builder to build this instance from */ NrsVvrItem(final NrsVvrItem.Builder builder) { super(builder); LOGGER.trace("building new {} instance with uuid {}", NrsVvrItem.class.getSimpleName(), this.getUuid()); this.vvr = Objects.requireNonNull(builder.vvr); this.persistenceFile = newPersistenceFile(builder.metadataDirectory, getUuid()); this.nrsFile = builder.nrsFile; final NrsFileHeader<NrsFile> header = nrsFile.getDescriptor(); final UuidT<NrsFile> parent = header.getParentId(); final long size = checkSize(vvr, header.getSize()); { final UuidT<NrsFile> parentIdBuilder = builder.parentFileId; if (parentIdBuilder != null) { if (!String.valueOf(parent).equals(String.valueOf(parentIdBuilder))) { throw new IllegalStateException("Persistent item has wrong parent ID"); } } } if (vvr.getBlockSize() != header.getBlockSize()) { throw new IllegalStateException("Wrong block size=" + header.getBlockSize() + ", vvr=" + vvr.getBlockSize()); } if (vvr.getHashLength() != header.getHashSize()) { throw new IllegalStateException("Wrong hash length=" + header.getHashSize() + ", vvr=" + vvr.getHashLength()); } if (builder.size != 0 && builder.size != size) { throw new IllegalStateException("Wrong size=" + size + ", expected=" + builder.size); } // Type in remote messages if (this instanceof Device) { msgObjectType = Type.DEVICE; } else if (this instanceof Snapshot) { msgObjectType = Type.SNAPSHOT; } else { throw new AssertionError("class=" + getClass()); } } /** * Allocate and set field to save. * * @return values to save. */ protected Properties getPersistenceProperties() { final Properties result = new Properties(); result.setProperty(UUID_KEY, getUuid().toString()); // Do not set the values when they are null final String name = getName(); if (name != null) { result.setProperty(NAME_KEY, name); } final String desc = getDescription(); if (desc != null) { result.setProperty(DESC_KEY, desc); } return result; } final void create() throws IOException { // Create persistence file persist(); } final void persist() throws IOException { final Properties properties = getPersistenceProperties(); try (FileOutputStream fos = new FileOutputStream(persistenceFile)) { properties.store(fos, ""); } } final void unpersist() { // Delete persistence persistenceFile.delete(); } /** * Create the Java File containing the persistence of an item. * * @param persistenceDirectory * @param uuid * @return the file containing the persistence of the item */ private static final File newPersistenceFile(final File persistenceDirectory, final UUID uuid) { return new File(persistenceDirectory, uuid.toString()); } /** * Load the persistence for an item. * * @param persistenceDirectory * @param uuid * @return the persistence for the given UUID or null if there is no persistence. */ static final Properties loadPersistence(final File persistenceDirectory, final UUID uuid) { final File persistenceFile = newPersistenceFile(persistenceDirectory, uuid); if (!persistenceFile.exists()) { return null; } final Properties properties = new Properties(); try (FileInputStream fis = new FileInputStream(persistenceFile)) { properties.load(fis); } catch (final IOException e) { throw new IllegalStateException("Failed to load persistence '" + persistenceFile + "'", e); } return properties; } final NrsFile getNrsFilePath() { return nrsFile; } /** * Create a new {@link NrsFile} for this item, based on the given NrsFileHeader. * * @param nrsFileHeader */ final void createNewNrsFile(final NrsFileHeader<NrsFile> nrsFileHeader) { try { // Seal the local NrsFile final NrsFileJanitor nrsFileJanitor = vvr.getNrsFileJanitor(); nrsFileJanitor.sealNrsFile(nrsFile); final UuidT<NrsFile> prevNrsFileUuid = nrsFile.getDescriptor().getFileId(); final NrsFile nrsFileTmp = nrsFileJanitor.createNrsFile(nrsFileHeader); // Clear 'old' parentUuid resetParent(); nrsFile = nrsFileTmp; vvr.registerNrsFile(prevNrsFileUuid, nrsFile); } catch (final IOException e) { throw new IllegalStateException(e); } } /** * Open the NrsFile read/write. To be called from a device only. * * @throws IOException * @throws IllegalStateException */ final void openNrsFile() throws IllegalStateException, IOException { final UuidT<NrsFile> uuid = nrsFile.getDescriptor().getFileId(); nrsFile = getVvr().getNrsFileJanitor().openNrsFile(uuid, false); assert uuid.equals(nrsFile.getDescriptor().getFileId()); } /** * Open the NrsFile read-only. <b>To be called from unit tests only.</b> * * @throws IOException * @throws IllegalStateException */ final void openNrsFileTest() throws IllegalStateException, IOException { final UuidT<NrsFile> uuid = nrsFile.getDescriptor().getFileId(); nrsFile = getVvr().getNrsFileJanitor().openNrsFile(uuid, true); assert uuid.equals(nrsFile.getDescriptor().getFileId()); } /** * Close the {@link NrsFile}. Call from a device only. */ final void closeNrsFile() { closeNrsFile(false); } /** * Close the {@link NrsFile}. Call from a device only. */ final void closeNrsFile(final boolean setReadOnly) { getVvr().getNrsFileJanitor().closeNrsFile(nrsFile, setReadOnly); } /** * Delete the {@link NrsFile}. Call from a device only. */ final void deleteNrsFile() { try { getVvr().getNrsFileJanitor().deleteNrsFile(nrsFile); } catch (final IOException e) { throw new IllegalStateException(e); } } /** * Tells if the {@link NrsFile} of the item is locked. * * @return <code>true</code> if the file is locked */ final boolean isNrsFileLocked() { // Close the file if not locked and check if it's still opened getVvr().getNrsFileJanitor().flushNrsFile(nrsFile); return nrsFile.isOpened(); } @Override public final FutureVoid setUserProperties(final String... keyValues) { final Item.Builder builder = Item.newBuilder(); // Write key/value pairs final int count = keyValues.length; if (count < 1 || (count % 2 != 0)) { throw new IllegalArgumentException("keyValues.count=" + count); } for (int i = 0; i < count; i++) { builder.addSetProp(keyValues[i]); } return submitUserPropertyTransaction(builder); } @Override public final FutureVoid unsetUserProperties(final String... keys) { final Item.Builder builder = Item.newBuilder(); // Write key for (int i = keys.length - 1; i >= 0; i--) { builder.addDelProp(keys[i]); } return submitUserPropertyTransaction(builder); } private final FutureVoid submitUserPropertyTransaction(final Item.Builder builder) { final UUID itemUuid = getUuid(); final RemoteOperation.Builder builderOp = RemoteOperation.newBuilder(); builderOp.setItem(builder); builderOp.setUuid(VvrRemoteUtils.newUuid(itemUuid)); final NrsRepository repository = getVvr(); final UUID taskId = repository.submitTransaction(builderOp, msgObjectType, OpCode.SET); return new NrsFutureVoid(repository, taskId, itemUuid); } /** * Set a user-defined property. Executes a transaction. * * @param name * @param value */ final void setUserPropertiesLocal(final String name, final String value) { try { synchronized (persistenceFile) { Files.setUserAttr(persistenceFile.toPath(), name, value); } } catch (final IOException e) { throw new IllegalStateException("Failed to write user property '" + persistenceFile + "'", e); } } /** * Unset a user-defined property. Executes a transaction. * * @param name */ final void unsetUserPropertiesLocal(final String name) { try { synchronized (persistenceFile) { Files.unsetUserAttr(persistenceFile.toPath(), name); } } catch (final IOException e) { throw new IllegalStateException("Failed to unset user property '" + persistenceFile + "' name=" + name, e); } } @Override public final String getUserProperty(final String name) { try { synchronized (persistenceFile) { final Path persistencePath = persistenceFile.toPath(); return Files.getUserAttr(persistencePath, Objects.requireNonNull(name)); } } catch (final IOException e) { throw new IllegalStateException("Failed to get user property '" + persistenceFile + "'", e); } } @Override public final Map<String, String> getUserProperties() { try { synchronized (persistenceFile) { final Path persistencePath = persistenceFile.toPath(); final String[] attrs = Files.listUserAttr(persistencePath); final Map<String, String> result = new HashMap<>(attrs.length); for (int i = 0; i < attrs.length; i++) { final String attr = attrs[i]; result.put(attr, Files.getUserAttr(persistencePath, attr)); } return result; } } catch (final IOException e) { throw new IllegalStateException("Failed to get user properties '" + persistenceFile + "'", e); } } @Override public final NrsRepository getVvr() { return this.vvr; } @Override public final int getBlockSize() { return vvr.getBlockSize(); } @Override public final UUID getParent() { if (parentUuid != null) { return parentUuid; } // Look for the parent snapshot in the repository final Snapshot parent = vvr.getParentSnapshot(getParentFile()); parentUuid = parent.getUuid(); return parentUuid; } /** * Reset the parent UUID cache. */ protected final void resetParent() { parentUuid = null; } @Override public final boolean isPartial() { return nrsFile.getDescriptor().isPartial(); } @Override public final long getSize() { return nrsFile.getDescriptor().getSize(); } @Override public final long getDataSize() { // TODO: add corresponding method to NrsFile and base computation on that return nrsFile.getAllocatedNumberOfRecords() * getBlockSize(); } final Object readHash(final long blockIndex, final boolean recursive, final boolean ex) throws IOException { byte[] result = nrsFile.read(blockIndex); if (result != null) { if (ex) { return new NrsBlockKeyLookupEx(result, nrsFile); } else { return result; } } UUID nodeSrc = null; NrsFile fileSrc = null; if (result == null && recursive && isPartial()) { final NrsFileJanitor nrsFileJanitor = getVvr().getNrsFileJanitor(); UuidT<NrsFile> parentUuid = getParentFile(); boolean partial = true; while (partial) { // Get parent file final NrsFile parent = nrsFileJanitor.loadNrsFile(parentUuid); final NrsFileHeader<NrsFile> header = parent.getDescriptor(); // No need to try to access to the root snapshot (is empty) if (header.isRoot()) { break; } try { final NrsFile parentOpened = nrsFileJanitor.openNrsFile(parentUuid, true); try { result = parentOpened.read(blockIndex); } finally { nrsFileJanitor.unlockNrsFile(parentOpened); } // Key found: keep the node on which the file was filled if (result != null) { fileSrc = parentOpened; nodeSrc = parentOpened.getDescriptor().getNodeId(); break; } } catch (final IndexOutOfBoundsException ie) { LOGGER.debug("Read offset out of parent range"); // TODO: improve reading out of parent limits break; } // Upper level parentUuid = header.getParentId(); partial = header.isPartial(); } } // Not found, recursive or not if (result == null) { return null; } // Found, recursive only if (ex) { return new NrsBlockKeyLookupEx(result, fileSrc, nodeSrc); } else { return result; } } /** * Writes a block hash to the internal block mapping. * * @param blockIndex * index of the block to write * @param blockHash * the hash value to write * @throws IOException */ final void writeBlockHash(final long blockIndex, final byte[] blockHash) throws IOException { nrsFile.write(blockIndex, blockHash); } /** * Release the block written at the given index. Does nothing if the block is not allocated. * * @param blockIndex * @throws IOException */ final void resetBlockHash(final long blockIndex) throws IOException { nrsFile.reset(blockIndex); } /** * Trim the block written at the given index. Does nothing if the block is not allocated. * * @param blockIndex * @throws IOException */ final void trimBlockHash(final long blockIndex) throws IOException { nrsFile.trim(blockIndex); } /** * Gets the parent {@link NrsFile}. * * @return the direct parent in Nrs hierarchy */ public final UuidT<NrsFile> getParentFile() { return nrsFile.getDescriptor().getParentId(); } /** * Validates and sets a new value for the size of this item. * * @param newSize * the new size in bytes, must be a multiple of the configured block size */ private static final long checkSize(final NrsRepository vvr, final long newSize) { final int blockSize = vvr.getBlockSize(); if ((newSize != 0) && (newSize < blockSize)) { throw new IllegalArgumentException("newSize=" + newSize + ", blockSize=" + blockSize); } if ((newSize % blockSize) != 0) { throw new IllegalArgumentException("newSize=" + newSize + ", blockSize=" + blockSize); } return newSize; } /** * Gets the current frontend ID. * * @return the frontend ID or null if not defined */ public final UUID getDeviceId() { return nrsFile.getDescriptor().getDeviceId(); } /** * Gets the originating node of the item. * * @return the ID of the originating node or null if not defined */ public final UUID getNodeId() { return nrsFile.getDescriptor().getNodeId(); } /** * File ID. * * @return the <code>UUID</code> of the {@link NrsFile} */ final UuidT<NrsFile> getNrsFileId() { return nrsFile.getDescriptor().getFileId(); } final long getTimestamp() { return nrsFile.getDescriptor().getTimestamp(); } @Override protected final FutureVoid submitTransaction(final RemoteOperation.Builder opBuilder, final OpCode opCode) { final NrsRepository targetVvr = getVvr(); opBuilder.setUuid(newUuid(getUuid())); return new NrsFutureVoid(targetVvr, targetVvr.submitTransaction(opBuilder, msgObjectType, opCode), getUuid()); } @Override public final String toString() { return MoreObjects.toStringHelper(this).add("id", this.getUuid()) .add("name", this.getName()).add("vvrId", this.getVvr()).add("parent", this.getParent()) .add("parentItem", this.getParentFile()).add("partial", this.isPartial()).add("size", this.getSize()) .add("data size", this.getDataSize()).toString(); } /** * Parent builder to create or to load {@link NrsVvrItem}s. */ abstract static class Builder extends AbstractUniqueVvrObject.Builder { /** * VVR instance of the new item belongs to. */ private NrsRepository vvr; /** * Gets the configured VVR ID. * * @return the configured VVR ID */ protected final NrsRepository getVvr() { return this.vvr; } /** * Sets the VVR owning the future item. * <p> * * <i>REQUIRED.</i> * <p> * * @param vvr * the non-<code>null</code> existing VVR * @return the modified builder */ protected final NrsVvrItem.Builder vvr(final @Nonnull NrsRepository vvr) { this.vvr = Objects.requireNonNull(vvr); return this; } /** * The id of the parent item for the item to be built. */ private UuidT<NrsFile> parentFileId; protected final VvrItem.Builder parentFile(final UuidT<NrsFile> parentFile) { this.parentFileId = parentFile; return this; } /** * Valid {@link NrsFile}. */ private NrsFile nrsFile; /** * Sets the {@link NrsFile} associated to the item. Must be set after load of the persistence (before build()) * or before the creation. * * @param file * the {@link NrsFile} * @return the modified builder */ protected final NrsVvrItem.Builder sourceFile(@Nonnull final NrsFile nrsFile) { this.nrsFile = Objects.requireNonNull(nrsFile); return this; } /** * The size of the item in bytes. */ private long size = -1L; protected final VvrItem.Builder size(final long size) { assert size >= 0; this.size = size; return this; } private Set<NrsFileFlag> flags; protected final VvrItem.Builder flags(final Set<NrsFileFlag> flags) { this.flags = Collections.unmodifiableSet(flags); return this; } /** * The directory in which the metadata of the item will be saved. */ private File metadataDirectory; protected final NrsVvrItem.Builder metadataDirectory(final File metadataDirectory) { assert metadataDirectory.isDirectory(); this.metadataDirectory = metadataDirectory; return this; } protected final NrsFile createNrsFile(final NrsFileHeader<NrsFile> nrsFileHeader) { try { final NrsFileJanitor nrsFileJanitor = vvr.getNrsFileJanitor(); if (nrsFileHeader == null) { return nrsFileJanitor.createNrsFile(createNrsFileHeader()); } else { return nrsFileJanitor.createNrsFile(nrsFileHeader); } } catch (final NrsException e) { throw new IllegalStateException(e); } } protected final NrsFileHeader<NrsFile> createDefaultNrsFileHeader() { final Set<NrsFileFlag> flags = EnumSet.noneOf(NrsFileFlag.class); flags.add(NrsFileFlag.PARTIAL); if (NrsDevice.NRS_BLOCK_FILE_ENABLED) { flags.add(NrsFileFlag.BLOCKS); } return createNrsFileHeader(flags); } protected final NrsFileHeader<NrsFile> createNrsFileHeader() { return createNrsFileHeader(flags); } private final NrsFileHeader<NrsFile> createNrsFileHeader(final Set<NrsFileFlag> flags) { final NrsFileJanitor nrsFileJanitor = vvr.getNrsFileJanitor(); final NrsFileHeader.Builder<NrsFile> headerBuilder = nrsFileJanitor.newNrsFileHeaderBuilder(); // Ids headerBuilder.parent(parentFileId); headerBuilder.device(deviceID()); headerBuilder.node(nodeID()); headerBuilder.file(futureID()); // Data format headerBuilder.setFlags(flags).blockSize(vvr.getBlockSize()).hashSize(vvr.getHashLength()); // Size and time stamp headerBuilder.timestamp(System.currentTimeMillis()); headerBuilder.size(size); return headerBuilder.build(); } /** * Gets the device ID to create the NrsFile. * * @return the device ID */ protected abstract UUID deviceID(); /** * Gets the id of the originating node. * * @return ID of the originating node of the item */ protected abstract UUID nodeID(); /** * Gets the ID of the NrsFile to create. * * @return the file ID */ protected abstract UuidT<NrsFile> futureID(); } }