/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* 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.
*/
package org.uberfire.backend.server;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpSession;
import org.jboss.errai.bus.server.annotations.Service;
import org.jboss.errai.bus.server.api.RpcContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.uberfire.backend.server.util.Paths;
import org.uberfire.backend.vfs.Path;
import org.uberfire.backend.vfs.PathFactory;
import org.uberfire.backend.vfs.VFSLockService;
import org.uberfire.backend.vfs.impl.LockInfo;
import org.uberfire.backend.vfs.impl.LockResult;
import org.uberfire.io.IOService;
import org.uberfire.java.nio.IOException;
import org.uberfire.java.nio.file.DirectoryStream.Filter;
import org.uberfire.java.nio.file.FileSystem;
import org.uberfire.java.nio.file.Files;
import org.uberfire.java.nio.file.NoSuchFileException;
import org.uberfire.rpc.SessionInfo;
import org.uberfire.workbench.events.ResourceDeletedEvent;
import org.uberfire.workbench.events.ResourceRenamedEvent;
/**
* Errai RPC endpoint exposing a {@link VFSLockService}.
*/
@Service
@ApplicationScoped
public class VFSLockServiceImpl implements VFSLockService {
public static final String LOCK_SESSION_ATTRIBUTE_NAME = "uf-locks";
private static final Logger logger = LoggerFactory.getLogger(VFSLockServiceImpl.class);
@Inject
@Named("configIO")
private IOService ioService;
@Inject
@Named("systemFS")
private FileSystem fileSystem;
@Inject
private SessionInfo sessionInfo;
@Override
public LockResult acquireLock(final Path path)
throws IllegalArgumentException, IOException, UnsupportedOperationException {
try {
ioService.startBatch(fileSystem);
final String userId = sessionInfo.getIdentity().getIdentifier();
final LockInfo lockInfo = retrieveLockInfo(path);
final LockResult result;
if (lockInfo.isLocked() && !lockInfo.lockedBy().equals(userId)) {
result = LockResult.failed(lockInfo);
} else {
ioService.write(Paths.convert(lockInfo.getLock()),
userId);
result = LockResult.acquired(path,
userId);
updateSession(result.getLockInfo());
}
return result;
} finally {
ioService.endBatch();
}
}
@Override
public LockResult releaseLock(final Path path)
throws IllegalArgumentException, IOException {
return releaseLock(path,
false);
}
@Override
public LockResult forceReleaseLock(final Path path)
throws IllegalArgumentException, IOException {
final String userId = sessionInfo.getIdentity().getIdentifier();
logger.info("User " + userId + " forced a lock release of: " + path.toURI());
return releaseLock(path,
true);
}
private LockResult releaseLock(final Path path,
final boolean force)
throws IllegalArgumentException, IOException {
try {
ioService.startBatch(fileSystem);
final LockInfo lockInfo = retrieveLockInfo(path);
final LockResult result;
if (lockInfo.isLocked()) {
if (sessionInfo.getIdentity().getIdentifier().equals(lockInfo.lockedBy()) || force) {
ioService.delete(Paths.convert(lockInfo.getLock()));
updateSession(lockInfo,
true);
result = LockResult.released(path);
} else {
logger.error("Client requested to release a lock it doesn't hold: " + path.toURI());
throw new IOException("Not allowed");
}
} else {
result = LockResult.failed(lockInfo);
}
return result;
} finally {
ioService.endBatch();
}
}
@Override
public LockInfo retrieveLockInfo(Path path)
throws IllegalArgumentException, IOException {
final Path vfsLock = PathFactory.newLock(path);
final org.uberfire.java.nio.file.Path realLock = Paths.convert(vfsLock);
if (ioService.exists(realLock)) {
try {
final String lockedBy = ioService.readAllString(realLock);
return new LockInfo(true,
lockedBy,
path,
vfsLock);
} catch (NoSuchFileException nsfe) {
// We want to avoid starting a batch (to ensure cluster-wide consistent reads) here since
// this method is invoked very frequently. Therefore it's possible that the lock file
// was deleted after the check to exists but before readAllString was invoked. There's
// no need for special exception handling as it simply means that file is no longer locked.
}
}
return new LockInfo(false,
null,
path,
vfsLock);
}
@Override
public List<LockInfo> retrieveLockInfos(Path path,
boolean excludeOwnedLocks)
throws IllegalArgumentException, IOException {
if (!Files.isDirectory(Paths.convert(path))) {
return Collections.emptyList();
}
final Path lockPath = PathFactory.newLockPath(path);
final List<Path> locks = new ArrayList<Path>();
retrieveLocks(ioService.get(URI.create(lockPath.toURI())),
locks);
final List<LockInfo> lockInfos = new LinkedList<LockInfo>();
for (Path lock : locks) {
final LockInfo lockInfo = retrieveLockInfo(PathFactory.fromLock(lock));
if (!excludeOwnedLocks || !sessionInfo.getIdentity().getIdentifier().equals(lockInfo.lockedBy())) {
if (Files.exists(Paths.convert(lockInfo.getFile()))) {
lockInfos.add(lockInfo);
}
}
}
return lockInfos;
}
private void retrieveLocks(final org.uberfire.java.nio.file.Path path,
final List<Path> accu) {
if (!Files.exists(path)) {
return;
}
Filter<org.uberfire.java.nio.file.Path> filter = new Filter<org.uberfire.java.nio.file.Path>() {
@Override
public boolean accept(final org.uberfire.java.nio.file.Path entry) throws org.uberfire.java.nio.IOException {
if (Paths.convert(entry).toURI().endsWith(PathFactory.LOCK_FILE_EXTENSION)) {
accu.add(Paths.convert(entry));
} else if (Files.isDirectory(entry)) {
retrieveLocks(ioService.get(entry.toUri()),
accu);
}
return true;
}
};
Iterator<org.uberfire.java.nio.file.Path> it = ioService.newDirectoryStream(path,
filter).iterator();
while (it.hasNext()) {
it.next();
}
}
/**
* Updates the user's session to track all currently held locks so we can
* release locks on session expiry.
* @param lockInfo the lock to update
* @param remove true to remove the lock, false to add it
*/
private void updateSession(final LockInfo lockInfo,
boolean remove) {
final HttpSession session = RpcContext.getHttpSession();
@SuppressWarnings("unchecked")
Set<LockInfo> locks = (Set<LockInfo>) session.getAttribute(LOCK_SESSION_ATTRIBUTE_NAME);
if (remove) {
if (locks != null) {
locks.remove(lockInfo);
}
} else {
if (locks == null) {
locks = new HashSet<LockInfo>();
}
locks.add(lockInfo);
session.setAttribute(LOCK_SESSION_ATTRIBUTE_NAME,
locks);
}
}
private void updateSession(final LockInfo lockInfo) {
updateSession(lockInfo,
false);
}
@SuppressWarnings("unused")
private void onResourceDeleted(@Observes ResourceDeletedEvent res) {
maybeDeleteLock(res.getPath());
}
@SuppressWarnings("unused")
private void onResourceRenamed(@Observes ResourceRenamedEvent res) {
maybeDeleteLock(res.getPath());
}
private void maybeDeleteLock(final Path path) {
try {
ioService.startBatch(fileSystem);
final LockInfo lockInfo = retrieveLockInfo(path);
if (lockInfo.isLocked()) {
ioService.delete(Paths.convert(lockInfo.getLock()));
}
} finally {
ioService.endBatch();
}
}
}