001/* 002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Antoine Taillefer <[email protected]> 018 */ 019package org.nuxeo.wopi.lock; 020 021import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_DOC_ID; 022import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_FILE_ID; 023import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_LOCK; 024import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_NAME; 025import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_REPOSITORY; 026import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_SCHEMA_NAME; 027import static org.nuxeo.wopi.Constants.LOCK_DIRECTORY_TIMESTAMP; 028import static org.nuxeo.wopi.Constants.LOCK_TTL; 029 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.function.Consumer; 035import java.util.function.Function; 036 037import org.apache.logging.log4j.LogManager; 038import org.apache.logging.log4j.Logger; 039import org.nuxeo.ecm.core.api.DocumentModel; 040import org.nuxeo.ecm.core.api.repository.RepositoryManager; 041import org.nuxeo.ecm.core.query.sql.model.Predicates; 042import org.nuxeo.ecm.core.query.sql.model.QueryBuilder; 043import org.nuxeo.ecm.directory.Session; 044import org.nuxeo.ecm.directory.api.DirectoryService; 045import org.nuxeo.runtime.api.Framework; 046import org.nuxeo.wopi.Constants; 047import org.nuxeo.wopi.FileInfo; 048 049/** 050 * @since 10.3 051 */ 052public class LockHelper { 053 054 private static final Logger log = LogManager.getLogger(LockHelper.class); 055 056 /** 057 * Flag to know if the request originated from a WOPI client. 058 */ 059 protected static ThreadLocal<Boolean> isWOPIRequest = new ThreadLocal<>(); 060 061 private LockHelper() { 062 // helper class 063 } 064 065 /** 066 * Stores the given WOPI lock for the given file id with a timestamp for expiration purpose. 067 */ 068 public static void addLock(String fileId, String lock) { 069 FileInfo fileInfo = new FileInfo(fileId); 070 addLock(fileId, fileInfo.repositoryName, fileInfo.docId, lock); 071 } 072 073 /** 074 * @see #addLock(String, String) 075 */ 076 public static void addLock(String fileId, String repository, String docId, String lock) { 077 log.debug("Locking: fileId={} Adding lock {}", fileId, lock); 078 doPrivilegedOnLockDirectory(session -> { 079 Map<String, Object> entryMap = new HashMap<>(); 080 entryMap.put(LOCK_DIRECTORY_FILE_ID, fileId); 081 entryMap.put(LOCK_DIRECTORY_REPOSITORY, repository); 082 entryMap.put(LOCK_DIRECTORY_DOC_ID, docId); 083 entryMap.put(LOCK_DIRECTORY_LOCK, lock); 084 entryMap.put(LOCK_DIRECTORY_TIMESTAMP, System.currentTimeMillis()); 085 session.createEntry(entryMap); 086 }); 087 } 088 089 /** 090 * Gets the WOPI lock stored for the given file id if it exists, returns {@code null} otherwise. 091 */ 092 public static String getLock(String fileId) { 093 return doPrivilegedOnLockDirectory(session -> { 094 DocumentModel entry = session.getEntry(fileId); 095 return entry == null ? null : (String) entry.getProperty(LOCK_DIRECTORY_SCHEMA_NAME, LOCK_DIRECTORY_LOCK); 096 }); 097 } 098 099 /** 100 * Checks if a WOPI lock is stored for the given repository and doc id, no matter the xpath. 101 */ 102 public static boolean isLocked(String repository, String docId) { 103 QueryBuilder queryBuilder = new QueryBuilder().predicate(Predicates.eq(LOCK_DIRECTORY_REPOSITORY, repository)) 104 .and(Predicates.eq(LOCK_DIRECTORY_DOC_ID, docId)); 105 return doPrivilegedOnLockDirectory(session -> !session.query(queryBuilder, false).isEmpty()); 106 } 107 108 /** 109 * Checks if a WOPI lock is stored for the given file id. 110 */ 111 public static boolean isLocked(String fileId) { 112 return doPrivilegedOnLockDirectory(session -> session.getEntry(fileId) != null); 113 } 114 115 /** 116 * Updates the WOPI lock stored for the given file id with the given lock and a fresh timestamp. 117 */ 118 public static void updateLock(String fileId, String lock) { 119 log.debug("Locking: fileId={} Updating lock {}", fileId, lock); 120 doPrivilegedOnLockDirectory(session -> { 121 DocumentModel entry = session.getEntry(fileId); 122 entry.setProperty(LOCK_DIRECTORY_SCHEMA_NAME, LOCK_DIRECTORY_LOCK, lock); 123 entry.setProperty(LOCK_DIRECTORY_SCHEMA_NAME, LOCK_DIRECTORY_TIMESTAMP, System.currentTimeMillis()); 124 session.updateEntry(entry); 125 }); 126 } 127 128 /** 129 * Updates the WOPI lock stored for the given file id with a fresh timestamp. 130 */ 131 public static void refreshLock(String fileId) { 132 log.debug("Locking: fileId={} Refreshing lock", fileId); 133 doPrivilegedOnLockDirectory(session -> { 134 DocumentModel entry = session.getEntry(fileId); 135 entry.setProperty(LOCK_DIRECTORY_SCHEMA_NAME, LOCK_DIRECTORY_TIMESTAMP, System.currentTimeMillis()); 136 session.updateEntry(entry); 137 }); 138 } 139 140 /** 141 * Removes the WOPI lock stored for the given file id. 142 */ 143 public static void removeLock(String fileId) { 144 log.debug("Locking: fileId={} Removing lock", fileId); 145 doPrivilegedOnLockDirectory((Session session) -> session.deleteEntry(fileId)); 146 } 147 148 /** 149 * Removes all the WOPI locks stored for the given repository and doc id. 150 */ 151 public static void removeLocks(String repository, String docId) { 152 log.debug("Locking: repository={} docId={} Document was unlocked in Nuxeo, removing related WOPI locks", 153 repository, docId); 154 QueryBuilder queryBuilder = new QueryBuilder().predicate(Predicates.eq(LOCK_DIRECTORY_REPOSITORY, repository)) 155 .and(Predicates.eq(LOCK_DIRECTORY_DOC_ID, docId)); 156 doPrivilegedOnLockDirectory( 157 (Session session) -> session.query(queryBuilder, false).forEach(session::deleteEntry)); 158 } 159 160 /** 161 * Performs the given consumer with a privileged session on the lock directory. 162 */ 163 public static void doPrivilegedOnLockDirectory(Consumer<Session> consumer) { 164 Framework.doPrivileged(() -> { 165 try (Session session = openLockDirectorySession()) { 166 consumer.accept(session); 167 } 168 }); 169 } 170 171 /** 172 * Applies the given function with a privileged session on the lock directory. 173 */ 174 public static <R> R doPrivilegedOnLockDirectory(Function<Session, R> function) { 175 return Framework.doPrivileged(() -> { 176 try (Session session = openLockDirectorySession()) { 177 return function.apply(session); 178 } 179 }); 180 } 181 182 /** 183 * Returns the list of expired stored WOPI locks according to the {@link Constants#LOCK_TTL} for each repository. 184 * <p> 185 * The given session must be privileged. 186 */ 187 public static Map<String, List<DocumentModel>> getExpiredLocksByRepository(Session session) { 188 return Framework.getService(RepositoryManager.class) 189 .getRepositoryNames() 190 .stream() 191 .map(repository -> getExpiredLocks(session, repository)) 192 .reduce(new HashMap<String, List<DocumentModel>>(), (a, b) -> { 193 a.putAll(b); 194 return a; 195 }); 196 } 197 198 /** 199 * Returns {@code true} if the request originated from a WOPI client. 200 */ 201 public static boolean isWOPIRequest() { 202 return Boolean.TRUE.equals(isWOPIRequest.get()); 203 } 204 205 /** 206 * Flags the request as originating from a WOPI client. 207 */ 208 public static void flagWOPIRequest() { 209 isWOPIRequest.set(true); 210 } 211 212 /** 213 * Unflags the request as originating from a WOPI client. 214 */ 215 public static void unflagWOPIRequest() { 216 isWOPIRequest.remove(); 217 } 218 219 protected static Map<String, List<DocumentModel>> getExpiredLocks(Session session, String repository) { 220 long expirationTime = System.currentTimeMillis() - LOCK_TTL; 221 QueryBuilder queryBuilder = new QueryBuilder().predicate(Predicates.eq(LOCK_DIRECTORY_REPOSITORY, repository)) 222 .and(Predicates.lt(LOCK_DIRECTORY_TIMESTAMP, expirationTime)); 223 List<DocumentModel> expiredLocks = session.query(queryBuilder, false); 224 return Collections.singletonMap(repository, expiredLocks); 225 } 226 227 protected static Session openLockDirectorySession() { 228 return Framework.getService(DirectoryService.class).open(LOCK_DIRECTORY_NAME); 229 } 230 231}