All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.modeshape.jcr.RepositoryLockManager Maven / Gradle / Ivy

There is a newer version: 5.4.1.Final
Show newest version
/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.jcr;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.logging.Logger;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.ChildReference;
import org.modeshape.jcr.cache.LockFailureException;
import org.modeshape.jcr.cache.MutableCachedNode;
import org.modeshape.jcr.cache.NodeCache;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.SessionCache;
import org.modeshape.jcr.cache.change.Change;
import org.modeshape.jcr.cache.change.ChangeSet;
import org.modeshape.jcr.cache.change.ChangeSetListener;
import org.modeshape.jcr.cache.change.NodeAdded;
import org.modeshape.jcr.cache.change.NodeRemoved;
import org.modeshape.jcr.value.DateTimeFactory;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.Property;
import org.modeshape.jcr.value.PropertyFactory;

@ThreadSafe
class RepositoryLockManager implements ChangeSetListener {

    private static final int KEY_OFFSET = "mode:lock-".length();

    private final JcrRepository.RunningState repository;
    private final String systemWorkspaceName;
    private final String processId;
    private final ConcurrentMap locksByNodeKey;
    private final Path locksPath;
    private final Logger logger;

    RepositoryLockManager( JcrRepository.RunningState repository ) {
        this.repository = repository;
        this.systemWorkspaceName = repository.repositoryCache().getSystemWorkspaceName();
        this.processId = repository.context().getProcessId();
        this.locksByNodeKey = new ConcurrentHashMap();
        PathFactory pathFactory = repository.context().getValueFactories().getPathFactory();
        this.locksPath = pathFactory.create(pathFactory.createRootPath(), JcrLexicon.SYSTEM, ModeShapeLexicon.LOCKS);
        this.logger = Logger.getLogger(getClass());
    }

    RepositoryLockManager with( JcrRepository.RunningState repository ) {
        assert this.systemWorkspaceName == repository.repositoryCache().getSystemWorkspaceName();
        assert this.processId == repository.context().getProcessId();
        PathFactory pathFactory = repository.context().getValueFactories().getPathFactory();
        Path locksPath = pathFactory.create(pathFactory.createRootPath(), JcrLexicon.SYSTEM, ModeShapeLexicon.LOCKS);
        assert this.locksPath.equals(locksPath);
        return new RepositoryLockManager(repository);
    }

    /**
     * Refresh the locks from the stored representation.
     */
    protected void refreshFromSystem() {
        try {
            // Re-read and re-register all of the namespaces ...
            SessionCache systemCache = repository.createSystemSession(repository.context(), true);
            SystemContent system = new SystemContent(systemCache);
            CachedNode locks = system.locksNode();
            for (ChildReference ref : locks.getChildReferences(systemCache)) {
                CachedNode node = systemCache.getNode(ref);
                if (node == null) {
                    logger.warn(JcrI18n.lockNotFound, ref.getKey());
                    continue;
                }
                ModeShapeLock lock = new ModeShapeLock(node, systemCache);
                locksByNodeKey.put(lock.getLockedNodeKey(), lock);
            }
        } catch (Throwable e) {
            logger.error(e, JcrI18n.errorRefreshingLocks, repository.name());
        }
    }

    /**
     * Clean up the locks within the repository's system content. Any locks held by active sessions are extended/renewed, while
     * those locks that are significantly expired are removed.
     *
     * @param activeSessionIds the IDs of the sessions that are still active in this repository
     */
    protected void cleanupLocks( Set activeSessionIds ) {
        try {
            ExecutionContext context = repository.context();

            DateTimeFactory dates = context.getValueFactories().getDateFactory();
            DateTime now = dates.create();
            DateTime newExpiration = dates.create(now, RepositoryConfiguration.LOCK_EXTENSION_INTERVAL_IN_MILLIS);

            PropertyFactory propertyFactory = context.getPropertyFactory();
            SessionCache systemSession = repository.createSystemSession(context, false);
            SystemContent systemContent = new SystemContent(systemSession);

            Map> lockedNodesByWorkspaceName = new HashMap<>();
            // Iterate over the locks ...
            MutableCachedNode locksNode = systemContent.mutableLocksNode();
            for (ChildReference ref : locksNode.getChildReferences(systemSession)) {
                NodeKey lockKey = ref.getKey();
                CachedNode lockNode = systemSession.getNode(lockKey);
                if (lockNode == null) {
                    //it may happen that another thread has performed a session.logout which means the lock might have been removed
                    continue;
                }
                ModeShapeLock lock = new ModeShapeLock(lockNode, systemSession);
                NodeKey lockedNodeKey = lock.getLockedNodeKey();
                if (lock.isSessionScoped() && activeSessionIds.contains(lock.getLockingSessionId())) {
                    //for active session locks belonging to the sessions of this process, we want to extend the expiration date
                    //so that other processes in a cluster can tell that this lock is still active
                    MutableCachedNode mutableLockNode = systemSession.mutable(lockKey);
                    Property prop = propertyFactory.create(ModeShapeLexicon.EXPIRATION_DATE, newExpiration);
                    mutableLockNode.setProperty(systemSession, prop);
                    //reflect the change in the expiry date in the internal map
                    this.locksByNodeKey.replace(lockedNodeKey, lock.withExpiryTime(newExpiration));

                    continue;
                }

                //if it's not an active session lock, we always check the expiry date
                DateTime expirationDate = firstDate(lockNode.getProperty(ModeShapeLexicon.EXPIRATION_DATE, systemSession));
                if (expirationDate.isBefore(now)) {
                    //remove the lock from the system area
                    systemContent.removeLock(lock);
                    //register the target node which needs cleaning
                    List lockedNodes = lockedNodesByWorkspaceName.get(lock.getWorkspaceName());
                    if (lockedNodes == null) {
                        lockedNodes = new ArrayList<>();
                        lockedNodesByWorkspaceName.put(lock.getWorkspaceName(), lockedNodes);
                    }
                    lockedNodes.add(lockedNodeKey);
                }
            }
            //persist all the changes to the locks from the system area
            systemSession.save();

            //update each of nodes which has been unlocked
            for (String workspaceName : lockedNodesByWorkspaceName.keySet()) {
                SessionCache internalSession = repository.repositoryCache().createSession(context, workspaceName, false);
                for (NodeKey lockedNodeKey : lockedNodesByWorkspaceName.get(workspaceName)) {
                    //clear the internal cache
                    this.locksByNodeKey.remove(lockedNodeKey);

                    CachedNode lockedNode = internalSession.getWorkspace().getNode(lockedNodeKey);
                    if (lockedNode != null) {
                        MutableCachedNode mutableLockedNode = internalSession.mutable(lockedNodeKey);
                        mutableLockedNode.removeProperty(internalSession, JcrLexicon.LOCK_IS_DEEP);
                        mutableLockedNode.removeProperty(internalSession, JcrLexicon.LOCK_OWNER);
                        mutableLockedNode.unlock();
                    }
                }
                internalSession.save();
            }
        } catch (Throwable t) {
            logger.error(t, JcrI18n.errorCleaningUpLocks, repository.name());
        }
    }

    protected final NodeKey lockedNodeKeyFromLockKey( NodeKey key ) {
        // The identifier of the lock key contains "mode:lock-" followed by the full key of the locked node ...
        String identifier = key.getIdentifier();
        return new NodeKey(identifier.substring(KEY_OFFSET));
    }

    final boolean isLocked( NodeKey lockedNodeKey ) {
        return locksByNodeKey.containsKey(lockedNodeKey);
    }

    final ModeShapeLock findLockFor( NodeKey nodeKey ) {
        return locksByNodeKey.get(nodeKey);
    }

    private final CachedNode findLockedNodeAtOrBelow( CachedNode node,
                                                      NodeCache cache ) {
        if (node.getChildReferences(cache).isEmpty()) {
            // It is a leaf node, so just check for a lock on the node ...
            if (isLocked(node.getKey())) return node;
        }
        // We assume that there are far fewer locks than there are descendants of the supplied path ...
        Path path = node.getPath(cache);
        for (ModeShapeLock lock : locksByNodeKey.values()) {
            CachedNode lockedNode = cache.getNode(lock.getLockedNodeKey());
            if (lockedNode == null) continue;
            Path lockedPath = lockedNode.getPath(cache);
            if (lockedPath.isAtOrBelow(path)) return lockedNode;
        }
        return null;
    }

    final Collection allLocks() {
        return locksByNodeKey.values();
    }

    protected final NodeKey generateLockKey( NodeKey prototype,
                                             NodeKey lockedNodeKey ) {
        return prototype.withId("mode:lock-" + lockedNodeKey.toString());
    }

    protected final String generateLockToken() {
        return UUID.randomUUID().toString();
    }

    /**
     * Creates a lock on the node with the given {@link NodeKey key}. This method creates a new lock, registers it in the list of
     * locks, immediately modifies the {@code jcr:lockOwner} and {@code jcr:lockIsDeep} properties on the persisted node in the
     * underlying repository, and adds the lock to the persistent system view.
     * 

* This method assumes that the node is lockable and not already locked. *

* * @param session the session in which the node is being locked and that loaded the node * @param node the node that is being locked; may not be null * @param isDeep whether the node's descendants in the content graph should also be locked * @param isSessionScoped whether the lock should outlive the session in which it was created * @param timeoutHint desired lock timeout in seconds (servers are free to ignore this value); specify {@link Long#MAX_VALUE} * for no timeout. * @param ownerInfo a string containing owner information supplied by the client, and recorded on the lock; if null, then the * session's user ID will be used * @return an object representing the newly created lock * @throws LockException if the lock could not be obtained * @throws RepositoryException if an error occurs updating the graph state */ ModeShapeLock lock( JcrSession session, CachedNode node, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerInfo ) throws LockException, RepositoryException { assert session != null; assert node != null; final ExecutionContext context = session.context(); final String owner = ownerInfo != null ? ownerInfo : session.getUserID(); final DateTimeFactory dateFactory = context.getValueFactories().getDateFactory(); long expirationTimeInMillis = RepositoryConfiguration.LOCK_EXPIRY_AGE_IN_MILLIS; if (timeoutHint > 0 && timeoutHint < Long.MAX_VALUE) { expirationTimeInMillis = TimeUnit.MILLISECONDS.convert(timeoutHint, TimeUnit.SECONDS); } DateTime expirationDate = dateFactory.create().plus(expirationTimeInMillis, TimeUnit.MILLISECONDS); // Create a new lock ... SessionCache systemSession = repository.createSystemSession(context, false); SystemContent system = new SystemContent(systemSession); NodeKey nodeKey = node.getKey(); NodeKey lockKey = generateLockKey(system.locksKey(), nodeKey); String token = generateLockToken(); ModeShapeLock lock = new ModeShapeLock(nodeKey, lockKey, session.workspaceName(), owner, token, isDeep, isSessionScoped, session.sessionId(), expirationDate); if (isDeep) { NodeCache cache = session.cache(); CachedNode locked = findLockedNodeAtOrBelow(node, cache); if (locked != null) { String nodePath = session.stringFactory().create(node.getPath(cache)); String descendantPath = session.stringFactory().create(locked.getPath(cache)); throw new LockException(JcrI18n.descendantAlreadyLocked.text(nodePath, descendantPath)); } } ModeShapeLock existing = locksByNodeKey.putIfAbsent(nodeKey, lock); if (existing != null) { NodeCache cache = session.cache(); CachedNode locked = cache.getNode(existing.getLockedNodeKey()); String lockedPath = session.stringFactory().create(locked.getPath(cache)); throw new LockException(JcrI18n.alreadyLocked.text(lockedPath)); } try { // Store the lock within the system area ... system.storeLock(lock); // Update the persistent node ... SessionCache lockingSession = session.spawnSessionCache(false); MutableCachedNode lockedNode = lockingSession.mutable(nodeKey); PropertyFactory propertyFactory = session.propertyFactory(); lockedNode.setProperty(lockingSession, propertyFactory.create(JcrLexicon.LOCK_OWNER, owner)); lockedNode.setProperty(lockingSession, propertyFactory.create(JcrLexicon.LOCK_IS_DEEP, isDeep)); lockedNode.lock(isSessionScoped); // Now save both sessions. This will fail with a LockFailureException if the locking failed ... lockingSession.save(systemSession, null); } catch (LockFailureException e) { // Someone must have snuck in and locked the node, and we just didn't receive notification of it yet ... String location = nodeKey.toString(); try { location = session.node(nodeKey, null).getPath(); } catch (Throwable t) { // couldn't come up with the path, so just use the key } locksByNodeKey.remove(nodeKey); throw new LockException(JcrI18n.alreadyLocked.text(location)); } catch (RuntimeException e) { locksByNodeKey.remove(nodeKey); throw new RepositoryException(e); } return lock; } /** * Updates the underlying repository directly (i.e., outside the scope of the {@link Session}) to mark the token for the given * lock as being held (or not held) by some {@link Session}. Note that this method does not identify which (if any) * session holds the token for the lock, just that some session holds the token for the lock. * * @param session the session on behalf of which the lock operation is being performed * @param lockToken the lock token for which the "held" status should be modified; may not be null * @param value the new value * @return true if the lock "held" status was successfully changed to the desired value, or false otherwise * @throws LockException if there is no such lock with the supplied token */ boolean setHeldBySession( JcrSession session, String lockToken, boolean value ) throws LockException { assert lockToken != null; // Create a system session to remove the locks ... final ExecutionContext context = session.context(); SessionCache systemSession = repository.createSystemSession(context, false); SystemContent system = new SystemContent(systemSession); // Mark the session as held/unheld ... if (!system.changeLockHeldBySession(lockToken, value)) { return false; } // Now save the session ... system.save(); return true; } String unlock( JcrSession session, NodeKey lockedNodeKey ) throws LockException { ModeShapeLock existing = locksByNodeKey.remove(lockedNodeKey); if (existing == null) { NodeCache cache = session.cache(); String location = session.stringFactory().create(cache.getNode(lockedNodeKey).getPath(cache)); throw new LockException(JcrI18n.notLocked.text(location)); } unlock(session, Collections.singleton(existing)); return existing.getLockToken(); } private void unlock( JcrSession session, Iterable locks ) { if (locks == null) return; // Create a system session to remove the locks ... final ExecutionContext context = session.context(); SessionCache systemSession = repository.createSystemSession(context, false); SystemContent system = new SystemContent(systemSession); // And create a separate session cache to change the locked nodes ... SessionCache lockingSession = session.spawnSessionCache(false); // Remove the locks ... for (ModeShapeLock lock : locks) { system.removeLock(lock); NodeKey lockedNodeKey = lock.getLockedNodeKey(); if (session.cache().getNode(lockedNodeKey) == null) { // the node on which the lock was placed, has been removed continue; } MutableCachedNode lockedNode = lockingSession.mutable(lockedNodeKey); lockedNode.removeProperty(lockingSession, JcrLexicon.LOCK_IS_DEEP); lockedNode.removeProperty(lockingSession, JcrLexicon.LOCK_OWNER); lockedNode.unlock(); } // Now save the two sessions ... lockingSession.save(systemSession, null); } /** * Unlocks all locks corresponding to the tokens held by the supplied session. * * @param session the session on behalf of which the lock operation is being performed * @return a {@link Set} of the lock tokens that have been cleaned (removed from the system area and the corresponding nodes * unlocked) * @throws RepositoryException if the session is not live */ Set cleanLocks( JcrSession session ) throws RepositoryException { Set lockTokens = session.lockManager().lockTokens(); List locks = null; for (ModeShapeLock lock : locksByNodeKey.values()) { if (lock.isSessionScoped() && lockTokens.contains(lock.getLockToken())) { if (locks == null) locks = new LinkedList(); locks.add(lock); } } Set cleanedTokens = null; if (locks != null) { cleanedTokens = new HashSet<>(locks.size()); // clear the locks which have been unlocked unlock(session, locks); for (ModeShapeLock lock : locks) { locksByNodeKey.remove(lock.getLockedNodeKey()); cleanedTokens.add(lock.getLockToken()); } } return cleanedTokens != null ? cleanedTokens : Collections.emptySet(); } @Override public void notify( ChangeSet changeSet ) { if (!systemWorkspaceName.equals(changeSet.getWorkspaceName())) { // The change does not affect the 'system' workspace, so skip it ... return; } if (processId.equals(changeSet.getProcessKey())) { // We generated these changes, so skip them ... return; } try { // Now process the changes ... Set locksToDelete = null; for (Change change : changeSet) { if (change instanceof NodeAdded) { NodeAdded added = (NodeAdded)change; Path addedPath = added.getPath(); if (locksPath.isAncestorOf(addedPath)) { // Get the name of the node type ... Map props = added.getProperties(); NodeKey lockKey = added.getKey(); ModeShapeLock lock = new ModeShapeLock(lockKey, props); locksByNodeKey.put(lock.getLockedNodeKey(), lock); } } else if (change instanceof NodeRemoved) { NodeRemoved removed = (NodeRemoved)change; Path removedPath = removed.getPath(); if (locksPath.isAncestorOf(removedPath)) { // This was a lock that was removed ... if (locksToDelete == null) locksToDelete = new HashSet(); // The key of the locked node is embedded in the lock key ... NodeKey lockedNodeKey = lockedNodeKeyFromLockKey(removed.getKey()); locksByNodeKey.remove(lockedNodeKey); } } // Lock nodes are never moved, and properties added or removed, and the only properties changed are those // related to lock expiration, which we don't care about. } } catch (Throwable e) { logger.error(e, JcrI18n.errorCleaningUpLocks, repository.name()); } } protected final String firstString( Property property ) { if (property == null) return null; return repository.context().getValueFactories().getStringFactory().create(property.getFirstValue()); } protected final boolean firstBoolean( Property property ) { if (property == null) return false; return repository.context().getValueFactories().getBooleanFactory().create(property.getFirstValue()); } protected final DateTime firstDate( Property property ) { if (property == null) return null; return repository.context().getValueFactories().getDateFactory().create(property.getFirstValue()); } final ModeShapeLock findLockByToken( String token ) { assert token != null; for (ModeShapeLock lock : locksByNodeKey.values()) { if (token.equals(lock.getLockToken())) { return lock; } } return null; } /** * Internal representation of a locked node. */ @Immutable public class ModeShapeLock { private final NodeKey lockedNodeKey; private final NodeKey lockKey; private final String lockOwner; private final String workspaceName; private final String lockToken; private final boolean deep; private final boolean sessionScoped; private final String lockingSessionId; private final DateTime expiryTime; protected ModeShapeLock( CachedNode lockNode, NodeCache cache ) { this.lockKey = lockNode.getKey(); this.lockedNodeKey = lockedNodeKeyFromLockKey(lockKey); this.workspaceName = firstString(lockNode.getProperty(ModeShapeLexicon.WORKSPACE, cache)); this.lockOwner = firstString(lockNode.getProperty(JcrLexicon.LOCK_OWNER, cache)); this.deep = firstBoolean(lockNode.getProperty(JcrLexicon.LOCK_IS_DEEP, cache)); this.sessionScoped = firstBoolean(lockNode.getProperty(ModeShapeLexicon.IS_SESSION_SCOPED, cache)); this.lockToken = firstString(lockNode.getProperty(ModeShapeLexicon.LOCK_TOKEN, cache)); this.lockingSessionId = firstString(lockNode.getProperty(ModeShapeLexicon.LOCKING_SESSION, cache)); this.expiryTime = firstDate(lockNode.getProperty(ModeShapeLexicon.EXPIRATION_DATE, cache)); } protected ModeShapeLock( NodeKey lockKey, Map properties ) { this.lockKey = lockKey; this.lockedNodeKey = lockedNodeKeyFromLockKey(lockKey); this.workspaceName = firstString(properties.get(ModeShapeLexicon.WORKSPACE)); this.lockOwner = firstString(properties.get(JcrLexicon.LOCK_OWNER)); this.deep = firstBoolean(properties.get(JcrLexicon.LOCK_IS_DEEP)); this.sessionScoped = firstBoolean(properties.get(ModeShapeLexicon.IS_SESSION_SCOPED)); this.lockToken = firstString(properties.get(ModeShapeLexicon.LOCK_TOKEN)); this.lockingSessionId = firstString(properties.get(ModeShapeLexicon.LOCKING_SESSION)); this.expiryTime = firstDate(properties.get(ModeShapeLexicon.EXPIRATION_DATE)); } protected ModeShapeLock( NodeKey lockedNodeKey, NodeKey lockKey, String workspace, String lockOwner, String lockToken, boolean deep, boolean sessionScoped, String lockingSessionId, DateTime expiryTime ) { this.lockedNodeKey = lockedNodeKey; this.lockKey = lockKey; this.lockOwner = lockOwner; this.workspaceName = workspace; this.deep = deep; this.sessionScoped = sessionScoped; this.lockToken = lockToken; this.lockingSessionId = lockingSessionId; this.expiryTime = expiryTime; } protected ModeShapeLock withExpiryTime(DateTime expiryTime) { return new ModeShapeLock(this.lockedNodeKey, this.lockKey, this.workspaceName, this.lockOwner, this.lockToken, this.deep, this.sessionScoped, this.lockingSessionId, expiryTime); } public boolean isLive() { return isLocked(lockedNodeKey); } public NodeKey getLockKey() { return lockKey; } public NodeKey getLockedNodeKey() { return lockedNodeKey; } public String getWorkspaceName() { return workspaceName; } public boolean isDeep() { return deep; } public String getLockOwner() { return lockOwner; } public boolean isSessionScoped() { return sessionScoped; } public String getLockToken() { return lockToken; } public String getLockingSessionId() { return lockingSessionId; } public DateTime getExpiryTime() { return expiryTime; } @SuppressWarnings( "synthetic-access" ) public Lock lockFor( final JcrSession session ) throws RepositoryException { final AbstractJcrNode node = session.node(lockedNodeKey, null); final JcrLockManager lockManager = session.workspace().getLockManager(); return new Lock() { @Override public String getLockOwner() { return lockOwner; } @Override public String getLockToken() { if (sessionScoped) return null; String token = ModeShapeLock.this.getLockToken(); return lockManager.hasLockToken(token) ? token : null; } protected final String lockToken() { return ModeShapeLock.this.getLockToken(); } @Override public Node getNode() { return node; } @Override public boolean isDeep() { return deep; } @Override public boolean isLive() { return ModeShapeLock.this.isLive(); } @Override public boolean isSessionScoped() { return sessionScoped; } @Override public void refresh() throws LockException { String token = lockToken(); if (!lockManager.hasLockToken(token)) { String location = null; try { location = node.getPath(); } catch (RepositoryException e) { location = lockedNodeKey.toString(); } throw new LockException(JcrI18n.notLocked.text(location)); } } @Override public long getSecondsRemaining() { return isLockOwningSession() ? Long.MAX_VALUE : Long.MIN_VALUE; } @Override public boolean isLockOwningSession() { String token = lockToken(); return lockManager.hasLockToken(token); } }; } @Override public String toString() { return "Lock " + lockKey + " for " + lockedNodeKey + " in '" + workspaceName + "' (" + (deep ? "deep," : "shallow;") + (sessionScoped ? "session;" : "global;") + "owner='" + lockOwner + "';token='" + lockToken + "';"; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy