package io.urmia.md.repo.psql;
/**
*
* Copyright 2014 by Amin Abbaspour
*
* This file is part of Urmia.io
*
* Urmia.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Urmia.io 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.
*
* You should have received a copy of the GNU General Public License
* along with Urmia.io. If not, see <http://www.gnu.org/licenses/>.
*/
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import io.urmia.md.repo.util.JdbcPool;
import io.urmia.md.repo.MetadataRepository;
import io.urmia.md.exception.MetadataException;
import io.urmia.md.exception.MetadataTimeoutException;
import io.urmia.md.model.storage.Etag;
import io.urmia.md.model.storage.ExtendedObjectAttributes;
import io.urmia.md.model.storage.FullObjectName;
import io.urmia.md.model.storage.ObjectName;
import io.urmia.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static io.urmia.md.repo.util.JdbcUtil.*;
@SuppressWarnings("JpaQueryApiInspection")
public class PsqlMetadataRepositoryImpl implements MetadataRepository {
private static final Logger log = LoggerFactory.getLogger(PsqlMetadataRepositoryImpl.class);
private final JdbcPool pool;
public PsqlMetadataRepositoryImpl(JdbcPool pool) {
this.pool = pool;
}
@Override
public Optional<FullObjectName> selectByName(ObjectName on) throws MetadataException {
log.trace("findByName: {}", on);
if(on.ns == ObjectName.Namespace.ROOT) // special kind of list, returning available namespaces
return Optional.of(new FullObjectName(on, ROOT_EOA));
Connection c = null;
PreparedStatement s = null;
ResultSet r = null;
try {
c = pool.connection(true);
s = c.prepareStatement("SELECT type, size, md5, etag, durability, mtime FROM objects WHERE owner = ? AND ns = ?::namespace " +
"AND parent = ? AND name = ? AND deleted = FALSE LIMIT 1");
s.setString(1, on.owner);
s.setString(2, on.ns.path);
s.setString(3, on.parent);
s.setString(4, on.name);
s.setQueryTimeout(TIMEOUT_READ);
r = s.executeQuery();
if (r.next())
return Optional.fromNullable(mapResultSet(on, r));
return Optional.absent();
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout fetching object: " + on, e);
} catch (SQLException e) {
throw new MetadataException("sql error fetching object: " + on, e);
} finally {
tryClose(r);
tryClose(s);
tryClose(c);
}
}
//public static FileTime ROOT_MTIME = FileTime.fromMillis(System.currentTimeMillis()); // todo: fix this. use registration time
public static ExtendedObjectAttributes ROOT_EOA = new ExtendedObjectAttributes(true, 0, "", "", 2, System.currentTimeMillis());
private List<FullObjectName> listNamespaces(String owner) {
ObjectName.Namespace[] namespaces = ObjectName.Namespace.list();
List<FullObjectName> objectNames = new ArrayList<FullObjectName>(namespaces.length);
for(ObjectName.Namespace ns : namespaces) {
ObjectName on = new ObjectName(owner, ns, "/", ns.path);
FullObjectName fon = new FullObjectName(on, ROOT_EOA);
objectNames.add(fon);
}
return objectNames;
}
@Deprecated // duplicated with below
private FullObjectName mapResultSet(ObjectName on, ResultSet r) throws SQLException {
final long size = r.getLong("size");
if (r.wasNull()) return null;
final String type = r.getString("type");
if (r.wasNull()) return null;
final String md5 = r.getString("md5");
if (r.wasNull()) return null;
final String etag = r.getString("etag");
if (r.wasNull()) return null;
final int durability = r.getInt("durability");
if (r.wasNull()) return null;
final Timestamp mtimestamp = r.getTimestamp("mtime");
if (r.wasNull()) return null;
final long mtime = mtimestamp.getTime();
final boolean dir = ObjectName.ObjectType.DIRECTORY.name().equalsIgnoreCase(type);
final ExtendedObjectAttributes eoa = new ExtendedObjectAttributes(dir, size, md5, etag, durability, mtime);
return new FullObjectName(on, eoa);
}
@Deprecated // duplicated with above
private List<FullObjectName> mapResultSet(ResultSet r) throws SQLException {
ImmutableList.Builder<FullObjectName> b = ImmutableList.builder();
while (r.next()) {
final String owner = r.getString("owner");
if (r.wasNull()) continue;
final String nss = r.getString("ns");
if (r.wasNull()) continue;
ObjectName.Namespace ns = ObjectName.Namespace.of(nss);
if(ns.unknown()) continue;
final String parent = r.getString("parent");
if (r.wasNull()) continue;
final String name = r.getString("name");
if (r.wasNull()) continue;
if(StringUtils.isBlank(name)) continue;
final String type = r.getString("type");
if (r.wasNull()) continue;
boolean dir = ObjectName.ObjectType.DIRECTORY.name().equalsIgnoreCase(type);
final long size = r.getLong("size");
if (r.wasNull()) continue;
final String md5 = r.getString("md5");
if (r.wasNull()) continue;
final String etag = r.getString("etag");
if (r.wasNull()) continue;
final int durability = r.getInt("durability");
if (r.wasNull()) return null;
final Timestamp mtimestamp = r.getTimestamp("mtime");
if (r.wasNull()) return null;
final long mtime = mtimestamp.getTime();
ExtendedObjectAttributes eoa = new ExtendedObjectAttributes(dir, size, md5, etag, durability, mtime);
b.add(new FullObjectName(owner, ns, parent, name, eoa));
}
return b.build();
}
private static final String PSQL_ERROR_CODE_unique_violation = "23505";
private boolean entryExists(SQLException e) {
return PSQL_ERROR_CODE_unique_violation.equals(e.getSQLState());
}
private void update(FullObjectName fon) throws MetadataException {
log.trace("update: {}", fon);
Connection c = null;
PreparedStatement s = null;
try {
c = pool.connection(false);
s = c.prepareStatement("UPDATE objects SET size=?, mtime=now(), md5=?, etag=? WHERE " +
"owner=? AND ns=?::namespace AND parent=? AND name=? AND type=?::object_type AND deleted=FALSE");
s.setLong(1, fon.attributes.size);
s.setString(2, fon.attributes.md5);
s.setString(3, fon.attributes.etag);
s.setString(4, fon.owner);
s.setString(5, fon.ns.path);
s.setString(6, fon.parent);
s.setString(7, fon.name);
s.setString(8, fon.attributes.dir ? "directory" : "file");
s.setQueryTimeout(TIMEOUT_READ);
final int row = s.executeUpdate();
if (row != 1)
throw new MetadataException("failed to update. affected row count does not match. for: " + fon + ", row: " + row);
log.debug("updated success: {}", fon);
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout insert object: " + fon, e);
} catch (SQLException e) {
throw new MetadataException("sql error insert object: " + fon + ", error code: " + e.getErrorCode() + ", sql state: " + e.getSQLState(), e);
} finally {
tryClose(s);
tryClose(c);
}
}
@Override
public void insert(FullObjectName fon) throws MetadataException {
log.trace("insert: {}", fon);
Connection c = null;
PreparedStatement s = null;
try {
c = pool.connection(false);
s = c.prepareStatement("INSERT INTO objects(owner, ns, parent, name, type, size, mtime, md5, etag, durability, deleted) VALUES" +
"(?, ?::namespace, ?, ?, ?::object_type, ?, now(), ?, ?, ?, false)");
s.setString(1, fon.owner);
s.setString(2, fon.ns.path);
s.setString(3, fon.parent);
s.setString(4, fon.name);
s.setString(5, fon.attributes.dir ? "directory" : "file");
s.setLong(6, fon.attributes.size);
s.setString(7, fon.attributes.md5);
s.setString(8, fon.attributes.etag);
s.setInt(9, fon.attributes.durability);
s.setQueryTimeout(TIMEOUT_READ);
final int row = s.executeUpdate();
if (row != 1)
throw new MetadataException("failed to insert. affected row count does not match. for: " + fon + ", row: " + row);
log.debug("insert success: {}", fon);
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout insert object: " + fon, e);
} catch (SQLException e) {
if(entryExists(e)) {
log.info("entry exits. updating: {}", fon);
update(fon);
return;
}
throw new MetadataException("sql error insert object: " + fon + ", error code: " + e.getErrorCode() + ", sql state: " + e.getSQLState(), e);
} finally {
tryClose(s);
tryClose(c);
}
}
@Override
public void insertStored(String location, Etag etag) throws MetadataException {
log.trace("insertStored: {} in {}", etag, location);
Connection c = null;
PreparedStatement s = null;
try {
c = pool.connection(false);
s = c.prepareStatement("INSERT INTO storage(location, etag, mtime) VALUES(?, ?, now())");
s.setString(1, location);
s.setString(2, etag.value);
s.setQueryTimeout(TIMEOUT_READ);
final int row = s.executeUpdate();
if (row != 1)
throw new MetadataException("failed to insertStored. affected row count does not match: " + row);
log.debug("insertStored success: {} in {}", etag, location);
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout insertStored", e);
} catch (SQLException e) {
throw new MetadataException("sql error insertStored", e);
} finally {
tryClose(s);
tryClose(c);
}
}
@Override
public void delete(ObjectName on) throws MetadataException {
log.trace("delete: {}", on);
Connection c = null;
PreparedStatement s = null;
try {
c = pool.connection(false);
s = c.prepareStatement("UPDATE objects SET deleted = TRUE WHERE owner = ? AND ns = ?::namespace " +
"AND parent = ? AND name = ? AND deleted = FALSE");
s.setString(1, on.owner);
s.setString(2, on.ns.path);
s.setString(3, on.parent);
s.setString(4, on.name);
s.setQueryTimeout(TIMEOUT_READ);
final int row = s.executeUpdate();
if (row != 1)
throw new MetadataException("failed to flag delete. affected row count does not match. for: " + on + ", row: " + row);
log.debug("delete success: {}", on);
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout delete object: " + on, e);
} catch (SQLException e) {
throw new MetadataException("sql error delete object: " + on, e);
} finally {
tryClose(s);
tryClose(c);
}
}
@Override
public List<FullObjectName> listDir(ObjectName on, int limit) throws MetadataException {
log.debug("listDir: {}", on);
if(on.ns == ObjectName.Namespace.ROOT) // special kind of list, returning available namespaces
return listNamespaces(on.owner);
Connection c = null;
PreparedStatement s = null;
ResultSet r = null;
try {
c = pool.connection(true);
s = c.prepareStatement("SELECT * FROM objects WHERE owner = ? AND ns = ?::namespace AND deleted = FALSE AND parent=? LIMIT ?");
s.setString(1, on.owner);
s.setString(2, on.ns.path);
s.setString(3, on.path);
s.setInt(4, limit);
s.setQueryTimeout(TIMEOUT_READ);
r = s.executeQuery();
return mapResultSet(r);
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout list object: " + on, e);
} catch (SQLException e) {
throw new MetadataException("sql error list object: " + on, e);
} finally {
tryClose(r);
tryClose(s);
tryClose(c);
}
}
@Override
public List<String> findStorageByName(ObjectName on) throws MetadataException {
Optional<FullObjectName> fon = selectByName(on);
return fon.isPresent() ? findStorageNameByEtag(fon.get().attributes.etag) : Collections.<String>emptyList();
}
@Override
public List<String> findStorageNameByEtag(String etag) throws MetadataException {
log.debug("findStorageNameByEtag: {}", etag);
Connection c = null;
PreparedStatement s = null;
ResultSet r = null;
try {
c = pool.connection(true);
s = c.prepareStatement("SELECT location FROM storage WHERE etag=?");
s.setString(1, etag);
s.setQueryTimeout(TIMEOUT_READ);
r = s.executeQuery();
return mapStorageName(r);
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout list all storage for etag: " + etag, e);
} catch (SQLException e) {
throw new MetadataException("sql error list all storage for etag: " + etag, e);
} finally {
tryClose(r);
tryClose(s);
tryClose(c);
}
}
private List<String> mapStorageName(ResultSet r) throws SQLException {
ImmutableList.Builder<String> b = ImmutableList.builder();
while (r.next()) {
final String location = r.getString("location");
if (r.wasNull()) continue;
b.add(location);
}
return b.build();
}
@Override
public boolean deletable(ObjectName on) throws MetadataException {
if(on.ns == ObjectName.Namespace.ROOT || StringUtils.isBlank(on.name))
return false;
log.trace("deletable: {}", on);
Connection c = null;
PreparedStatement s = null;
ResultSet r = null;
try {
c = pool.connection(true);
s = c.prepareStatement("SELECT count(*) FROM objects WHERE owner = ? AND ns = ?::namespace " +
"AND parent = ? AND deleted = FALSE LIMIT 1");
s.setString(1, on.owner);
s.setString(2, on.ns.path);
s.setString(3, on.path);
s.setQueryTimeout(TIMEOUT_READ);
r = s.executeQuery();
return r.next() && r.getInt(1) == 0;
} catch (SQLTimeoutException e) {
throw new MetadataTimeoutException("timeout fetching object: " + on, e);
} catch (SQLException e) {
throw new MetadataException("sql error fetching object: " + on, e);
} finally {
tryClose(r);
tryClose(s);
tryClose(c);
}
}
}