/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.vfs.impl.memory; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.ForbiddenException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.vfs.Archiver; import org.eclipse.che.api.vfs.HashSumsCounter; import org.eclipse.che.api.vfs.LockedFileFinder; import org.eclipse.che.api.vfs.Path; import org.eclipse.che.api.vfs.VirtualFile; import org.eclipse.che.api.vfs.VirtualFileFilter; import org.eclipse.che.api.vfs.VirtualFileSystem; import org.eclipse.che.api.vfs.VirtualFileVisitor; import org.eclipse.che.api.vfs.search.SearcherProvider; import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.commons.lang.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static java.util.Collections.singletonMap; /** * In-memory implementation of VirtualFile. * <p/> * NOTE: This implementation is not thread safe. * * @author andrew00x */ public class MemoryVirtualFile implements VirtualFile { private static final Logger LOG = LoggerFactory.getLogger(MemoryVirtualFile.class); private static final boolean FILE = false; private static final boolean FOLDER = true; static MemoryVirtualFile newFile(MemoryVirtualFile parent, String name, InputStream content) throws IOException { return new MemoryVirtualFile(parent, name, content == null ? new byte[0] : ByteStreams.toByteArray(content)); } static MemoryVirtualFile newFile(MemoryVirtualFile parent, String name, byte[] content) { return new MemoryVirtualFile(parent, name, content == null ? new byte[0] : Arrays.copyOf(content, content.length)); } static MemoryVirtualFile newFolder(MemoryVirtualFile parent, String name) { return new MemoryVirtualFile(parent, name); } // private final boolean type; private final Map<String, String> properties; private final Map<String, MemoryVirtualFile> children; private final MemoryVirtualFileSystem fileSystem; private String name; private MemoryVirtualFile parent; private byte[] content; private long lastModificationDate; private LockHolder lock; private boolean exists = true; // --- File --- private MemoryVirtualFile(MemoryVirtualFile parent, String name, byte[] content) { this.fileSystem = (MemoryVirtualFileSystem)parent.getFileSystem(); this.parent = parent; this.type = FILE; this.name = name; this.properties = newHashMap(); this.content = content; children = Collections.emptyMap(); } // --- Folder --- private MemoryVirtualFile(MemoryVirtualFile parent, String name) { this.fileSystem = (MemoryVirtualFileSystem)parent.getFileSystem(); this.parent = parent; this.type = FOLDER; this.name = name; this.properties = newHashMap(); children = newHashMap(); } // --- Root folder --- MemoryVirtualFile(VirtualFileSystem virtualFileSystem) { this.fileSystem = (MemoryVirtualFileSystem)virtualFileSystem; this.type = FOLDER; this.name = ""; this.properties = newHashMap(); children = newHashMap(); } @Override public String getName() { checkExistence(); return name; } @Override public Path getPath() { checkExistence(); MemoryVirtualFile parent = this.parent; if (parent == null) { return Path.ROOT; } Path parentPath = parent.getPath(); return parentPath.newPath(getName()); } @Override public boolean isFile() { checkExistence(); return type == FILE; } @Override public boolean isFolder() { checkExistence(); return type == FOLDER; } @Override public boolean exists() { return exists; } @Override public boolean isRoot() { checkExistence(); return parent == null; } @Override public long getLastModificationDate() { checkExistence(); return lastModificationDate; } @Override public VirtualFile getParent() { checkExistence(); return parent; } @Override public Map<String, String> getProperties() { checkExistence(); return newHashMap(properties); } @Override public String getProperty(String name) { checkExistence(); return properties.get(name); } @Override public VirtualFile updateProperties(Map<String, String> update, String lockToken) throws ForbiddenException { checkExistence(); if (isFile() && fileIsLockedAndLockTokenIsInvalid(lockToken)) { throw new ForbiddenException(String.format("Unable update properties of item '%s'. Item is locked", getPath())); } for (Map.Entry<String, String> entry : update.entrySet()) { if (entry.getValue() == null) { properties.remove(entry.getKey()); } else { properties.put(entry.getKey(), entry.getValue()); } } lastModificationDate = System.currentTimeMillis(); return this; } @Override public VirtualFile updateProperties(Map<String, String> properties) throws ForbiddenException, ServerException { return updateProperties(properties, null); } @Override public VirtualFile setProperty(String name, String value, String lockToken) throws ForbiddenException, ServerException { updateProperties(singletonMap(name, value), lockToken); return this; } @Override public VirtualFile setProperty(String name, String value) throws ForbiddenException, ServerException { return setProperty(name, value, null); } @Override public void accept(VirtualFileVisitor visitor) throws ServerException { checkExistence(); visitor.visit(this); } @Override public List<Pair<String, String>> countMd5Sums() throws ServerException { checkExistence(); if (isFile()) { return newArrayList(); } return new HashSumsCounter(this, Hashing.md5()).countHashSums(); } @Override public List<VirtualFile> getChildren(VirtualFileFilter filter) { checkExistence(); if (isFolder()) { return doGetChildren(this).stream().filter(filter::accept).sorted().collect(Collectors.toList()); } return newArrayList(); } @Override public List<VirtualFile> getChildren() { checkExistence(); if (isFolder()) { List<VirtualFile> children = doGetChildren(this); if (children.size() > 1) { Collections.sort(children); } return children; } return newArrayList(); } private List<VirtualFile> doGetChildren(VirtualFile folder) { return newArrayList(((MemoryVirtualFile)folder).children.values()); } @Override public boolean hasChild(Path path) throws ServerException { return getChild(path) != null; } @Override public VirtualFile getChild(Path path) throws ServerException { checkExistence(); MemoryVirtualFile child = this; Iterator<String> pathSegments = newArrayList(path.elements()).iterator(); while (pathSegments.hasNext() && child != null) { child = child.children.get(pathSegments.next()); } if (pathSegments.hasNext()) { return null; } return child; } boolean addChild(MemoryVirtualFile child) { checkExistence(); final String childName = child.getName(); if (children.get(childName) == null) { children.put(childName, child); return true; } return false; } @Override public InputStream getContent() throws ForbiddenException { return new ByteArrayInputStream(getContentAsBytes()); } @Override public byte[] getContentAsBytes() throws ForbiddenException { checkExistence(); if (isFile()) { if (content == null) { content = new byte[0]; } return Arrays.copyOf(content, content.length); } throw new ForbiddenException(String.format("We were unable to retrieve the content. Item '%s' is not a file", getPath())); } @Override public String getContentAsString() throws ForbiddenException { return new String(getContentAsBytes()); } @Override public VirtualFile updateContent(InputStream content, String lockToken) throws ForbiddenException, ServerException { byte[] bytes; try { bytes = ByteStreams.toByteArray(content); } catch (IOException e) { throw new ServerException(String.format("We were unable to set the content of '%s'. Error: %s", getPath(), e.getMessage())); } doUpdateContent(bytes, lockToken); return this; } @Override public VirtualFile updateContent(byte[] content, String lockToken) throws ForbiddenException, ServerException { doUpdateContent(content, lockToken); return this; } @Override public VirtualFile updateContent(String content, String lockToken) throws ForbiddenException, ServerException { return updateContent(content.getBytes(), lockToken); } @Override public VirtualFile updateContent(byte[] content) throws ForbiddenException, ServerException { return updateContent(content, null); } @Override public VirtualFile updateContent(InputStream content) throws ForbiddenException, ServerException { return updateContent(content, null); } @Override public VirtualFile updateContent(String content) throws ForbiddenException, ServerException { return updateContent(content, null); } private void doUpdateContent(byte[] content, String lockToken) throws ForbiddenException, ServerException { checkExistence(); if (isFile()) { if (fileIsLockedAndLockTokenIsInvalid(lockToken)) { throw new ForbiddenException( String.format("We were unable to update the content of file '%s'. The file is locked", getPath())); } this.content = Arrays.copyOf(content, content.length); lastModificationDate = System.currentTimeMillis(); updateInSearcher(); } else { throw new ForbiddenException(String.format("We were unable to update the content. Item '%s' is not a file", getPath())); } } @Override public long getLength() { checkExistence(); if (isFile()) { return content.length; } return 0; } @Override public VirtualFile copyTo(VirtualFile parent) throws ForbiddenException, ConflictException, ServerException { return copyTo(parent, null, false); } @Override public VirtualFile copyTo(VirtualFile parent, String newName, boolean overwrite) throws ForbiddenException, ConflictException, ServerException { checkExistence(); ((MemoryVirtualFile)parent).checkExistence(); if (isRoot()) { throw new ServerException("Unable copy root folder"); } if (newName == null || newName.trim().isEmpty()) { newName = this.getName(); } if (parent.isFolder()) { VirtualFile copy = doCopy((MemoryVirtualFile)parent, newName, overwrite); addInSearcher(copy); return copy; } else { throw new ForbiddenException(String.format("Unable create copy of '%s'. Item '%s' specified as parent is not a folder.", getPath(), parent.getPath())); } } private VirtualFile doCopy(MemoryVirtualFile parent, String newName, boolean overwrite) throws ConflictException, ForbiddenException, ServerException { if (overwrite) { MemoryVirtualFile existedItem = parent.children.get(newName); if (existedItem != null) { existedItem.delete(); } } MemoryVirtualFile virtualFile; if (isFile()) { virtualFile = newFile(parent, newName, Arrays.copyOf(content, content.length)); } else { virtualFile = newFolder(parent, newName); for (VirtualFile child : getChildren()) { child.copyTo(virtualFile); } } virtualFile.properties.putAll(this.properties); if (parent.addChild(virtualFile)) { return virtualFile; } throw new ConflictException(String.format("Item '%s' already exists", parent.getPath().newPath(newName))); } @Override public VirtualFile moveTo(VirtualFile parent) throws ForbiddenException, ConflictException, ServerException { return moveTo(parent, null, false, null); } @Override public VirtualFile moveTo(VirtualFile parent, String newName, boolean overwrite, String lockToken) throws ForbiddenException, ConflictException, ServerException { checkExistence(); MemoryVirtualFile memoryParent = (MemoryVirtualFile)parent; memoryParent.checkExistence(); if (isRoot()) { throw new ForbiddenException("Unable move root folder"); } if (!parent.isFolder()) { throw new ForbiddenException("Unable move item. Item specified as parent is not a folder"); } if (newName == null || newName.trim().isEmpty()) { newName = this.getName(); } final boolean isFile = isFile(); final Path myPath = getPath(); final Path newParentPath = parent.getPath(); final boolean folder = isFolder(); if (folder) { if (newParentPath.isChild(myPath)) { throw new ForbiddenException( String.format("Unable move item %s to %s. Item may not have itself as parent", myPath, newParentPath)); } final List<VirtualFile> lockedFiles = new LockedFileFinder(this).findLockedFiles(); if (!lockedFiles.isEmpty()) { throw new ForbiddenException( String.format("Unable move item '%s'. Child items '%s' are locked", getName(), lockedFiles)); } } else if (fileIsLockedAndLockTokenIsInvalid(lockToken)) { throw new ForbiddenException(String.format("Unable move item %s. Item is locked", myPath)); } if (overwrite) { MemoryVirtualFile existedItem = memoryParent.children.get(newName); if (existedItem != null) { existedItem.delete(); } } if (memoryParent.children.containsKey(newName)) { throw new ConflictException(String.format("Item '%s' already exists", parent.getPath().newPath(newName))); } this.parent.children.remove(name); memoryParent.children.put(newName, this); this.parent = memoryParent; this.name = newName; lock = null; deleteFromSearcher(myPath, isFile); addInSearcher(this); return this; } @Override public VirtualFile rename(String newName, String lockToken) throws ForbiddenException, ConflictException, ServerException { checkExistence(); checkName(newName); boolean isFile = isFile(); if (isRoot()) { throw new ForbiddenException("We were unable to rename a root folder."); } final Path myPath = getPath(); final boolean isFolder = isFolder(); if (isFolder) { final List<VirtualFile> lockedFiles = new LockedFileFinder(this).findLockedFiles(); if (!lockedFiles.isEmpty()) { throw new ForbiddenException( String.format("Unable rename item '%s'. Child items '%s' are locked", getName(), lockedFiles)); } } else { if (fileIsLockedAndLockTokenIsInvalid(lockToken)) { throw new ForbiddenException(String.format("We were unable to rename an item '%s'." + " The item is currently locked by the system", getPath())); } } if (parent.children.get(newName) != null) { throw new ConflictException(String.format("Item '%s' already exists", newName)); } parent.children.remove(name); parent.children.put(newName, this); name = newName; lock = null; lastModificationDate = System.currentTimeMillis(); deleteFromSearcher(myPath, isFile); addInSearcher(this); return this; } @Override public VirtualFile rename(String newName) throws ForbiddenException, ConflictException, ServerException { return rename(newName, null); } @Override public void delete(String lockToken) throws ForbiddenException, ServerException { checkExistence(); boolean isFile = isFile(); if (isRoot()) { throw new ForbiddenException("Unable delete root folder"); } final Path myPath = getPath(); final boolean folder = isFolder(); if (folder) { final List<VirtualFile> lockedFiles = new LockedFileFinder(this).findLockedFiles(); if (!lockedFiles.isEmpty()) { throw new ForbiddenException( String.format("Unable delete item '%s'. Child items '%s' are locked", getName(), lockedFiles)); } for (VirtualFile virtualFile : getTreeAsList(this)) { ((MemoryVirtualFile)virtualFile).exists = false; } } else { if (fileIsLockedAndLockTokenIsInvalid(lockToken)) { throw new ForbiddenException(String.format("Unable delete item '%s'. Item is locked", getPath())); } } parent.children.remove(name); exists = false; parent = null; deleteFromSearcher(myPath, isFile); } List<VirtualFile> getTreeAsList(VirtualFile folder) throws ServerException { List<VirtualFile> list = newArrayList(); folder.accept(new VirtualFileVisitor() { @Override public void visit(VirtualFile virtualFile) throws ServerException { if (virtualFile.isFolder()) { for (VirtualFile child : virtualFile.getChildren()) { child.accept(this); } } list.add(virtualFile); } }); return list; } @Override public void delete() throws ForbiddenException, ServerException { delete(null); } @Override public InputStream zip() throws ForbiddenException, ServerException { checkExistence(); if (isFolder()) { return compress(fileSystem.getArchiverFactory().createArchiver(this, "zip")); } else { throw new ForbiddenException(String.format("Unable export to zip. Item '%s' is not a folder", getPath())); } } @Override public void unzip(InputStream zipped, boolean overwrite, int stripNumber) throws ForbiddenException, ServerException, ConflictException { checkExistence(); if (isFolder()) { extract(fileSystem.getArchiverFactory().createArchiver(this, "zip"), zipped, overwrite, stripNumber); addInSearcher(this); } else { throw new ForbiddenException(String.format("Unable import zip. Item '%s' is not a folder", getPath())); } } @Override public InputStream tar() throws ForbiddenException, ServerException { checkExistence(); if (isFolder()) { return compress(fileSystem.getArchiverFactory().createArchiver(this, "tar")); } else { throw new ForbiddenException(String.format("Unable export to tar archive. Item '%s' is not a folder", getPath())); } } @Override public void untar(InputStream tarArchive, boolean overwrite, int stripNumber) throws ForbiddenException, ConflictException, ServerException { checkExistence(); if (isFolder()) { extract(fileSystem.getArchiverFactory().createArchiver(this, "tar"), tarArchive, overwrite, stripNumber); addInSearcher(this); } else { throw new ForbiddenException(String.format("Unable import tar archive. Item '%s' is not a folder", getPath())); } } private InputStream compress(Archiver archiver) throws ForbiddenException, ServerException { try { ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); archiver.compress(byteOut); return new ByteArrayInputStream(byteOut.toByteArray()); } catch (IOException e) { throw new ServerException(e.getMessage(), e); } } private void extract(Archiver archiver, InputStream compressed, boolean overwrite, int stripNumber) throws ConflictException, ServerException, ForbiddenException { try { archiver.extract(compressed, overwrite, stripNumber); } catch (IOException e) { throw new ServerException(e.getMessage(), e); } } @Override public String lock(long timeout) throws ForbiddenException, ConflictException { checkExistence(); if (isFile()) { if (this.lock != null) { throw new ConflictException("File already locked"); } final String lockToken = NameGenerator.generate(null, 32); this.lock = new LockHolder(lockToken, timeout); lastModificationDate = System.currentTimeMillis(); return lockToken; } else { throw new ForbiddenException(String.format("Unable lock '%s'. Locking allowed for files only", getPath())); } } @Override public VirtualFile unlock(String lockToken) throws ForbiddenException, ConflictException { checkExistence(); if (isFile()) { final LockHolder theLock = lock; if (theLock == null) { throw new ConflictException("File is not locked"); } else if (isExpired(theLock)) { lock = null; throw new ConflictException("File is not locked"); } if (theLock.lockToken.equals(lockToken)) { lock = null; lastModificationDate = System.currentTimeMillis(); } else { throw new ForbiddenException("Unable remove lock from file. Lock token does not match"); } lastModificationDate = System.currentTimeMillis(); return this; } else { throw new ForbiddenException(String.format("Unable unlock '%s'. Locking allowed for files only", getPath())); } } @Override public boolean isLocked() { checkExistence(); final LockHolder myLock = lock; if (myLock != null) { if (isExpired(myLock)) { lock = null; return false; } return true; } return false; } private boolean isExpired(LockHolder lockHolder) { return lockHolder.expired < System.currentTimeMillis(); } @Override public VirtualFile createFile(String name, InputStream content) throws ForbiddenException, ConflictException, ServerException { checkExistence(); checkName(name); if (Path.of(name).length() > 1) { throw new ServerException(String.format("Invalid name '%s'", name)); } if (isFolder()) { final MemoryVirtualFile newFile; try { newFile = newFile(this, name, content); } catch (IOException e) { throw new ServerException(String.format("Unable set content of '%s'. Error: %s", getPath(), e.getMessage())); } if (!addChild(newFile)) { throw new ConflictException(String.format("Item with the name '%s' already exists", name)); } addInSearcher(newFile); return newFile; } else { throw new ForbiddenException("Unable create new file. Item specified as parent is not a folder"); } } @Override public VirtualFile createFile(String name, byte[] content) throws ForbiddenException, ConflictException, ServerException { return createFile(name, new ByteArrayInputStream(content)); } @Override public VirtualFile createFile(String name, String content) throws ForbiddenException, ConflictException, ServerException { return createFile(name, content.getBytes()); } @Override public VirtualFile createFolder(String name) throws ForbiddenException, ConflictException, ServerException { checkExistence(); checkName(name); if (name.charAt(0) == '/') { name = name.substring(1); } checkName(name); if (isFolder()) { MemoryVirtualFile newFolder = null; MemoryVirtualFile current = this; if (name.indexOf('/') > 0) { final Path internPath = Path.of(name); for (String element : internPath.elements()) { MemoryVirtualFile folder = newFolder(current, element); if (current.addChild(folder)) { newFolder = folder; current = folder; } else { current = current.children.get(element); } } if (newFolder == null) { throw new ConflictException(String.format("Item with the name '%s' already exists", name)); } } else { newFolder = newFolder(this, name); if (!addChild(newFolder)) { throw new ConflictException(String.format("Item with the name '%s' already exists", name)); } } return newFolder; } else { throw new ForbiddenException("Unable create new folder. Item specified as parent is not a folder"); } } @Override public java.io.File toIoFile() { return null; } @Override public VirtualFileSystem getFileSystem() { return fileSystem; } @Override public int compareTo(VirtualFile o) { // To get nice order of items: // 1. Regular folders // 2. Files if (o == null) { throw new NullPointerException(); } if (isFolder()) { return o.isFolder() ? getName().compareTo(o.getName()) : -1; } else if (o.isFolder()) { return 1; } return getName().compareTo(o.getName()); } @Override public String toString() { return getPath().toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof MemoryVirtualFile)) { return false; } MemoryVirtualFile other = (MemoryVirtualFile)o; return Objects.equals(fileSystem, other.fileSystem) && Objects.equals(getPath(), other.getPath()); } @Override public int hashCode() { return Objects.hash(fileSystem, getPath()); } private void checkExistence() { if (!exists) { throw new RuntimeException(String.format("Item '%s' already removed", name)); } } private void checkName(String name) throws ServerException { if (name == null || name.trim().isEmpty()) { throw new ServerException("Item's name is not set"); } } private void addInSearcher(VirtualFile newFile) { SearcherProvider searcherProvider = fileSystem.getSearcherProvider(); if (searcherProvider != null) { try { searcherProvider.getSearcher(fileSystem).add(newFile); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } } private void updateInSearcher() { SearcherProvider searcherProvider = fileSystem.getSearcherProvider(); if (searcherProvider != null) { try { searcherProvider.getSearcher(fileSystem).update(this); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } } private void deleteFromSearcher(Path path, boolean isFile) { SearcherProvider searcherProvider = fileSystem.getSearcherProvider(); if (searcherProvider != null) { try { searcherProvider.getSearcher(fileSystem).delete(path.toString(), isFile); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } } private boolean fileIsLockedAndLockTokenIsInvalid(String lockToken) { if (isLocked()) { final LockHolder myLock = lock; return myLock != null && !myLock.lockToken.equals(lockToken); } return false; } private static class LockHolder { final String lockToken; final long expired; LockHolder(String lockToken, long timeout) { this.lockToken = lockToken; this.expired = timeout > 0 ? (System.currentTimeMillis() + timeout) : Long.MAX_VALUE; } } }