com.flexive.core.storage.genericSQL.GenericLockStorage.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.core.storage.genericSQL.GenericLockStorage.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.core.storage.genericSQL;

import com.flexive.core.Database;
import com.flexive.core.DatabaseConst;
import com.flexive.core.storage.ContentStorage;
import com.flexive.core.storage.LockStorage;
import com.flexive.core.storage.StorageManager;
import com.flexive.shared.CacheAdmin;
import com.flexive.shared.FxContext;
import com.flexive.shared.FxLock;
import com.flexive.shared.FxLockType;
import com.flexive.shared.content.*;
import com.flexive.shared.exceptions.*;
import com.flexive.shared.security.ACLPermission;
import com.flexive.shared.security.Account;
import com.flexive.shared.security.UserTicket;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import static com.flexive.core.DatabaseConst.TBL_LOCKS;

/**
 * Generic SQL based implementation of the lock storage
 *
 * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
public class GenericLockStorage implements LockStorage {
    private static final Log LOG = LogFactory.getLog(GenericLockStorage.class);

    public final static long DURATION_LOOSE = 10 * 60 * 1000; //10 min
    public final static long DURATION_PERM = 24 * 60 * 60 * 1000; //24 hr

    private static final LockStorage instance = new GenericLockStorage();

    /**
     * Singleton getter
     *
     * @return LockStorage
     */
    public static LockStorage getInstance() {
        return instance;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock lock(Connection con, FxLockType lockType, FxPK pk) throws FxLockException {
        switch (lockType) {
        case Loose:
            return lock(con, lockType, pk, DURATION_LOOSE);
        case Permanent:
            return lock(con, lockType, pk, DURATION_PERM);
        }
        throw new UnsupportedOperationException("Unsupported lock type: " + lockType.name());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock lock(Connection con, FxLockType lockType, FxPK pk, long duration) throws FxLockException {
        return _lock(con, lockType, pk, duration);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock lock(Connection con, FxLockType lockType, String resource) throws FxLockException {
        switch (lockType) {
        case Loose:
            return lock(con, lockType, resource, DURATION_LOOSE);
        case Permanent:
            return lock(con, lockType, resource, DURATION_PERM);
        }
        throw new UnsupportedOperationException("Unsupported lock type: " + lockType.name());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock lock(Connection con, FxLockType lockType, String resource, long duration) throws FxLockException {
        return _lock(con, lockType, resource, duration);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock getLock(Connection con, FxPK pk) throws FxLockException {
        return _getLock(con, pk);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock getLock(Connection con, String resource) throws FxLockException {
        return _getLock(con, resource);
    }

    /**
     * Resolve the distinct version of a primary key or throw an exception if no distinct version can be evaluated
     *
     * @param con an open and valid connection
     * @param pk  primary key
     * @return pk in a distinct version
     * @throws FxLockException if no distinct version could be evaluated
     */
    private FxPK getDistinctPK(Connection con, FxPK pk) throws FxLockException {
        if (pk.isDistinctVersion())
            return pk;
        final FxContentVersionInfo cvi;
        try {
            cvi = StorageManager.getContentStorage(pk.getStorageMode()).getContentVersionInfo(con, pk.getId());
        } catch (FxNotFoundException e) {
            throw new FxLockException(e);
        }
        if (pk.getVersion() == FxPK.LIVE) {
            if (cvi.hasLiveVersion())
                return new FxPK(pk.getId(), cvi.getLiveVersion());
        }
        if (pk.getVersion() == FxPK.MAX)
            return new FxPK(pk.getId(), cvi.getMaxVersion());
        throw new FxLockException("ex.lock.distictPK");
    }

    /**
     * Internal lock method that acquires a lock for a primary key or resource depending of the class of <code>obj</code>
     *
     * @param con      an open and valid connection
     * @param lockType type of the lock
     * @param obj      resource or primary key
     * @param duration duration in [ms] of the lock
     * @return FxLock
     * @throws FxLockException     on errors
     */
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    protected FxLock _lock(Connection con, FxLockType lockType, Object obj, long duration) throws FxLockException {
        if (obj instanceof FxPK) {
            obj = getDistinctPK(con, (FxPK) obj);
        } else if (obj instanceof String) {
            if (StringUtils.isEmpty((String) obj))
                throw new FxLockException("ex.lock.invalidResource");
        } else
            throw new FxLockException("ex.lock.invalidResource");
        PreparedStatement ps = null;

        final UserTicket ticket = FxContext.getUserTicket();
        //permission checks if content lock
        if (!(ticket.isGlobalSupervisor() || ticket.isMandatorSupervisor()) && obj instanceof FxPK)
            checkEditPermission(con, (FxPK) obj, ticket);

        final boolean allowTakeOver = lockType == FxLockType.Loose || (lockType == FxLockType.Permanent
                && (ticket.isGlobalSupervisor() || ticket.isMandatorSupervisor()));
        final long now = System.currentTimeMillis();

        try {
            FxLock currentLock = _getLock(con, obj);
            if (currentLock.isLocked()) {
                //if the lock already exist, extend it if it is from the same user or take over if allowed to
                if (currentLock.getUserId() == ticket.getUserId())
                    return extend(con, currentLock, duration);
                if (!allowTakeOver)
                    throw new FxLockException(
                            "ex.lock.takeOver.denied." + (obj instanceof FxPK ? "pk" : "resource"), obj);
                return takeOver(con, currentLock, duration);
            }
            if (obj instanceof FxPK) {
                ps = con.prepareStatement("INSERT INTO " + TBL_LOCKS +
                // 1       2                      3       4        5          6
                        " (LOCK_ID,LOCK_VER,LOCK_RESOURCE,USER_ID,LOCKTYPE,CREATED_AT,EXPIRES_AT)VALUES(?,?,NULL,?,?,?,?)");
                ps.setLong(1, ((FxPK) obj).getId());
                ps.setInt(2, ((FxPK) obj).getVersion());
                ps.setLong(3, ticket.getUserId());
                ps.setInt(4, lockType.getId());
                ps.setLong(5, now);
                ps.setLong(6, now + duration);
            } else {
                ps = con.prepareStatement("INSERT INTO " + TBL_LOCKS +
                //                  1             2       3        4          5
                        " (LOCK_ID,LOCK_VER,LOCK_RESOURCE,USER_ID,LOCKTYPE,CREATED_AT,EXPIRES_AT)VALUES(NULL,NULL,?,?,?,?,?)");
                ps.setString(1, (String) obj);
                ps.setLong(2, ticket.getUserId());
                ps.setInt(3, lockType.getId());
                ps.setLong(4, now);
                ps.setLong(5, now + duration);
            }
            if (ps.executeUpdate() != 1)
                throw new FxLockException("ex.lock.lockFailed.noRows." + (obj instanceof FxPK ? "pk" : "resource"),
                        obj);
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
        return new FxLock(lockType, now, now + duration, ticket.getUserId(), obj);
    }

    /**
     * Check if the calling user has edit permission on the primary key he is trying to lock
     *
     * @param con    an open and valid connection
     * @param pk     primary key of the content instance to check
     * @param ticket calling users ticket
     * @throws FxLockException thrown when the content can not be loaded/checked or no access
     */
    private void checkEditPermission(Connection con, FxPK pk, UserTicket ticket) throws FxLockException {
        if (ticket.isGuest() || ticket.getUserId() == Account.USER_GUEST)
            throw new FxLockException("ex.lock.content.guest");
        FxContent content;
        FxContentSecurityInfo si;
        FxCachedContent cachedContent = CacheAdmin.getCachedContent(pk);
        if (cachedContent == null) {
            StringBuilder sb = new StringBuilder(5000);
            try {
                final ContentStorage contentStorage = StorageManager.getContentStorage(pk.getStorageMode());
                content = contentStorage.contentLoad(con, pk, CacheAdmin.getEnvironment(), sb);
                si = contentStorage.getContentSecurityInfo(con, pk, content);
            } catch (FxApplicationException e) {
                throw new FxLockException(e);
            }
        } else {
            content = cachedContent.getContent();
            si = cachedContent.getSecurityInfo();
        }
        try {
            FxPermissionUtils.checkPermission(ticket, content.getLifeCycleInfo().getCreatorId(), ACLPermission.EDIT,
                    CacheAdmin.getEnvironment().getType(content.getTypeId()), si.getStepACL(), si.getContentACLs(),
                    true);
        } catch (FxNoAccessException e) {
            throw new FxLockException(e, "ex.lock.content.noEditPermission", pk);
        }
    }

    /**
     * Internal method that returns a lock if <code>obj</code> is locked, or <code>FxLockType.None</code> if not
     *
     * @param con an open and valid connection
     * @param obj resource or primary key
     * @return FxLock or <code>FxLockType.None</code> if not
     * @throws FxLockException on errors
     */
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    protected FxLock _getLock(Connection con, Object obj) throws FxLockException {
        if (obj instanceof FxPK) {
            obj = getDistinctPK(con, (FxPK) obj);
        } else if (obj instanceof String) {
            if (StringUtils.isEmpty((String) obj))
                throw new FxLockException("ex.lock.invalidResource");
        } else
            throw new FxLockException("ex.lock.invalidResource");
        PreparedStatement ps = null;
        try {
            if (obj instanceof FxPK) {
                //                                1       2        3          4
                ps = con.prepareStatement("SELECT USER_ID,LOCKTYPE,CREATED_AT,EXPIRES_AT FROM " + TBL_LOCKS
                        + " WHERE LOCK_ID=? AND LOCK_VER=?");
                ps.setLong(1, ((FxPK) obj).getId());
                ps.setInt(2, ((FxPK) obj).getVersion());
            } else {
                //                                1       2        3          4
                ps = con.prepareStatement("SELECT USER_ID,LOCKTYPE,CREATED_AT,EXPIRES_AT FROM " + TBL_LOCKS
                        + " WHERE LOCK_RESOURCE=?");
                ps.setString(1, (String) obj);
            }
            ResultSet rs = ps.executeQuery();
            if (rs == null || !rs.next())
                return (obj instanceof FxPK ? FxLock.noLockPK() : FxLock.noLockResource());
            FxLock ret = new FxLock(FxLockType.getById(rs.getInt(2)), rs.getLong(3), rs.getLong(4), rs.getLong(1),
                    obj);
            if (ret.isExpired()) {
                ps.close();
                if (ret.isContentLock()) {
                    ps = con.prepareStatement("DELETE FROM " + TBL_LOCKS + " WHERE LOCK_ID=? AND LOCK_VER=?");
                    ps.setLong(1, ((FxPK) obj).getId());
                    ps.setInt(2, ((FxPK) obj).getVersion());
                } else {
                    ps = con.prepareStatement("DELETE FROM " + TBL_LOCKS + " WHERE LOCK_RESOURCE=?");
                    ps.setString(1, String.valueOf(obj));
                }
                ps.executeUpdate();
                return (obj instanceof FxPK ? FxLock.noLockPK() : FxLock.noLockResource());
            }
            return ret;
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void unlock(Connection con, FxPK pk) throws FxLockException {
        _unlock(con, pk);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void unlock(Connection con, String resource) throws FxLockException {
        _unlock(con, resource);
    }

    /**
     * Internal unlock method to unlock a primary key or resource
     *
     * @param con an open and valid connection
     * @param obj resource or primary key
     * @throws FxLockException on errors
     */
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    protected void _unlock(Connection con, Object obj) throws FxLockException {
        FxLock currentLock = _getLock(con, obj);
        if (!currentLock.isLocked())
            return; //nothing locked, so unlock is successful
        final UserTicket ticket = FxContext.getUserTicket();
        if (!ticket.isGlobalSupervisor() && (ticket.isGuest() || ticket.getUserId() == Account.USER_GUEST))
            throw new FxLockException("ex.lock.content.guest");
        final boolean allowUnlock = currentLock.getLockType() == FxLockType.Loose || currentLock.isExpired()
                || (currentLock.getLockType() == FxLockType.Permanent
                        && (ticket.getUserId() == currentLock.getUserId() || ticket.isGlobalSupervisor()
                                || ticket.isMandatorSupervisor()));
        if (!allowUnlock)
            throw new FxLockException("ex.lock.unlock.denied", obj);
        PreparedStatement ps = null;
        try {
            if (currentLock.isContentLock()) {
                obj = getDistinctPK(con, (FxPK) obj); //make sure to have a distinct pk
                ps = con.prepareStatement("DELETE FROM " + TBL_LOCKS + " WHERE LOCK_ID=? AND LOCK_VER=?");
                ps.setLong(1, ((FxPK) obj).getId());
                ps.setInt(2, ((FxPK) obj).getVersion());
            } else {
                ps = con.prepareStatement("DELETE FROM " + TBL_LOCKS + " WHERE LOCK_RESOURCE=?");
                ps.setString(1, String.valueOf(obj));
            }
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    public FxLock extend(Connection con, FxLock lock, long duration) throws FxLockException {
        final UserTicket ticket = FxContext.getUserTicket();
        if (lock.isExpired()) {
            final Object obj = lock.isContentLock() ? lock.getLockedPK() : lock.getLockedResource();
            return _lock(con, lock.getLockType(), obj, duration);
        }
        final boolean allowExtend = lock.getLockType() == FxLockType.Loose
                || (lock.getLockType() == FxLockType.Permanent && (ticket.getUserId() == lock.getUserId()
                        || ticket.isGlobalSupervisor() || ticket.isMandatorSupervisor()));
        if (!allowExtend)
            throw new FxLockException("ex.lock.extend.denied." + (lock.isContentLock() ? "pk" : "resource"),
                    (lock.isContentLock() ? lock.getLockedPK() : lock.getLockedResource()));
        PreparedStatement ps = null;
        try {
            if (lock.isContentLock()) {
                ps = con.prepareStatement(
                        "UPDATE " + TBL_LOCKS + " SET EXPIRES_AT=? WHERE LOCK_ID=? AND LOCK_VER=?");
                ps.setLong(2, lock.getLockedPK().getId());
                ps.setInt(3, lock.getLockedPK().getVersion());
            } else {
                ps = con.prepareStatement("UPDATE " + TBL_LOCKS + " SET EXPIRES_AT=? WHERE LOCK_RESOURCE=?");
                ps.setString(2, lock.getLockedResource());
            }
            ps.setLong(1, lock.getExpiresTimestamp() + duration);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
        Object obj = lock.isContentLock() ? lock.getLockedPK() : lock.getLockedResource();
        return new FxLock(lock.getLockType(), lock.getCreatedTimestamp(), lock.getExpiresTimestamp() + duration,
                lock.getUserId(), obj);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FxLock takeOver(Connection con, FxLock lock) throws FxLockException {
        return takeOver(con, lock, -1);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    public FxLock takeOver(Connection con, FxLock lock, long duration) throws FxLockException {
        final UserTicket ticket = FxContext.getUserTicket();
        final boolean allowTakeOver = lock.getLockType() == FxLockType.Loose || lock.isExpired()
                || (lock.getLockType() == FxLockType.Permanent
                        && (ticket.isGlobalSupervisor() || ticket.isMandatorSupervisor()));
        if (!allowTakeOver) {
            if (lock.isContentLock())
                throw new FxLockException("ex.lock.takeOver.denied.pk", lock.getLockedPK());
            else
                throw new FxLockException("ex.lock.takeOver.denied.resource", lock.getLockedResource());
        }
        //permission checks if content lock
        if (!(ticket.isGlobalSupervisor() || ticket.isMandatorSupervisor()) && lock.isContentLock())
            checkEditPermission(con, lock.getLockedPK(), ticket);
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(
                    "UPDATE " + TBL_LOCKS + " SET USER_ID=?" + (duration > 0 ? ",EXPIRES_AT=?" : "") + " WHERE "
                            + (lock.isContentLock() ? "LOCK_ID=? AND LOCK_VER=?" : "LOCK_RESOURCE=?"));
            ps.setLong(1, ticket.getUserId());
            int startIdx = 2;
            if (duration > 0) {
                ps.setLong(2, lock.getExpiresTimestamp() + duration);
                startIdx++;
            }
            if (lock.isContentLock()) {
                ps.setLong(startIdx, lock.getLockedPK().getId());
                ps.setInt(startIdx + 1, lock.getLockedPK().getVersion());
            } else
                ps.setString(startIdx, lock.getLockedResource());
            if (ps.executeUpdate() != 1)
                throw new FxLockException("ex.lock.takeOverFailed.noRows",
                        lock.isContentLock() ? lock.getLockedPK() : lock.getLockedResource());
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
        Object obj = lock.isContentLock() ? lock.getLockedPK() : lock.getLockedResource();
        return new FxLock(lock.getLockType(), lock.getCreatedTimestamp(),
                duration > 0 ? lock.getExpiresTimestamp() + duration : lock.getExpiresTimestamp(),
                ticket.getUserId(), obj);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    public List<FxLock> getUserLocks(Connection con, long userId) {
        List<FxLock> result = new ArrayList<FxLock>(10);
        PreparedStatement ps = null;
        try {
            //                                1        2          3          4       5        6
            ps = con.prepareStatement("SELECT LOCKTYPE,CREATED_AT,EXPIRES_AT,LOCK_ID,LOCK_VER,LOCK_RESOURCE FROM "
                    + TBL_LOCKS + " WHERE USER_ID=?");
            ps.setLong(1, userId);
            ResultSet rs = ps.executeQuery();
            while (rs != null && rs.next()) {
                Object res = rs.getString(6);
                if (rs.wasNull())
                    res = new FxPK(rs.getLong(4), rs.getInt(5));
                try {
                    result.add(new FxLock(FxLockType.getById(rs.getInt(1)), rs.getLong(2), rs.getLong(3), userId,
                            res));
                } catch (FxLockException e) {
                    LOG.warn(e);
                }
            }
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    public List<FxLock> getLocks(Connection con, FxLockType lockType, long userId, long typeId, String resource) {
        final UserTicket ticket = FxContext.getUserTicket();
        if (!(ticket.isGlobalSupervisor() || ticket.isMandatorSupervisor()))
            userId = ticket.getUserId();

        StringBuilder sql = new StringBuilder(500);
        sql.append(
                "SELECT l.LOCKTYPE,l.CREATED_AT,l.EXPIRES_AT,l.LOCK_ID,l.LOCK_VER,l.LOCK_RESOURCE,l.USER_ID FROM ")
                .append(TBL_LOCKS).append(" l");
        boolean hasWhere = false;
        if (typeId >= 0) {
            hasWhere = true;
            sql.append(", ").append(DatabaseConst.TBL_CONTENT).append(" c");
            sql.append(" WHERE c.ID=l.LOCK_ID AND c.VER=l.LOCK_VER AND c.TDEF=").append(typeId);
        }
        if (lockType != null) {
            if (!hasWhere) {
                hasWhere = true;
                sql.append(" WHERE ");
            } else
                sql.append(" AND ");
            sql.append("l.LOCKTYPE=").append(lockType.getId());
        }
        if (userId >= 0) {
            if (!hasWhere) {
                hasWhere = true;
                sql.append(" WHERE ");
            } else
                sql.append(" AND ");
            sql.append("l.USER_ID=").append(userId);
        }
        if (!StringUtils.isEmpty(resource)) {
            if (!hasWhere) {
                sql.append(" WHERE ");
            } else
                sql.append(" AND ");
            resource = resource.trim();
            //prevent sql injection, although only callable by global supervisors
            resource = resource.replace('\'', '_');
            resource = resource.replace('\"', '_');
            resource = resource.replace('%', '_');
            sql.append("l.LOCK_RESOURCE LIKE '%").append(resource).append("%'");
        }
        List<FxLock> result = new ArrayList<FxLock>(50);
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(sql.toString());
            ResultSet rs = ps.executeQuery();
            while (rs != null && rs.next()) {
                Object res = rs.getString(6);
                if (rs.wasNull())
                    res = new FxPK(rs.getLong(4), rs.getInt(5));
                try {
                    result.add(new FxLock(FxLockType.getById(rs.getInt(1)), rs.getLong(2), rs.getLong(3),
                            rs.getLong(7), res));
                } catch (FxLockException e) {
                    LOG.warn(e);
                }
            }
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    public void removeExpiredLocks(Connection con) {
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement("DELETE FROM " + TBL_LOCKS + " WHERE EXPIRES_AT<?");
            ps.setLong(1, System.currentTimeMillis());
            int count = ps.executeUpdate();
            if (count > 0)
                LOG.info("Expired " + count + " locks");
        } catch (SQLException e) {
            throw new FxDbException(e, "ex.db.sqlError", e.getMessage()).asRuntimeException();
        } finally {
            Database.closeObjects(GenericLockStorage.class, null, ps);
        }
    }
}