/* * Copyright (c) 2010 Mysema Ltd. * All rights reserved. * */ package com.mysema.rdfbean.rdb; import static com.mysema.rdfbean.rdb.QLanguage.language; import static com.mysema.rdfbean.rdb.QStatement.statement; import static com.mysema.rdfbean.rdb.QSymbol.symbol; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.UUID; import javax.annotation.Nullable; import com.google.common.base.Function; import com.google.common.collect.BiMap; import com.mysema.commons.l10n.support.LocaleUtil; import com.mysema.commons.lang.CloseableIterator; import com.mysema.commons.lang.IteratorAdapter; import com.mysema.query.QueryMetadata; import com.mysema.query.Tuple; import com.mysema.query.dml.DeleteClause; import com.mysema.query.dml.StoreClause; import com.mysema.query.sql.SQLQuery; import com.mysema.query.sql.dml.SQLDeleteClause; import com.mysema.query.sql.dml.SQLMergeClause; import com.mysema.query.types.Expression; import com.mysema.query.types.FactoryExpression; import com.mysema.query.types.Visitor; import com.mysema.query.types.template.NumberTemplate; import com.mysema.rdfbean.model.BID; import com.mysema.rdfbean.model.ID; import com.mysema.rdfbean.model.InferenceOptions; import com.mysema.rdfbean.model.LIT; import com.mysema.rdfbean.model.NODE; import com.mysema.rdfbean.model.QueryLanguage; import com.mysema.rdfbean.model.QueryOptions; import com.mysema.rdfbean.model.RDFBeanTransaction; import com.mysema.rdfbean.model.RDFConnection; import com.mysema.rdfbean.model.STMT; import com.mysema.rdfbean.model.UID; import com.mysema.rdfbean.model.UpdateLanguage; /** * RDBConnection is the RDFConnection implementation for the RDB module * * @author tiwe * @version $Id$ */ public class RDBConnection implements RDFConnection { private static final int ADD_BATCH = 1000; private static final int DELETE_BATCH = 1000; private static final Timestamp DEFAULT_TIMESTAMP = new Timestamp(0); public static final QSymbol con = new QSymbol("context"); public static final QSymbol obj = new QSymbol("object"); public static final QSymbol pre = new QSymbol("predicate"); public static final QSymbol sub = new QSymbol("subject"); private final RDBContext context; private final long defaultDatatypeId; private final int defaultLocaleId; private final Function<Long, NODE> nodeTransformer = new Function<Long, NODE>() { @Override public NODE apply(Long id) { SQLQuery query = context.createQuery(); query.from(symbol); query.where(symbol.id.eq(id)); Tuple result = query.uniqueResult(symbol.resource, symbol.lexical, symbol.datatype, symbol.lang); if (result != null) { return getNode( result.get(symbol.resource), result.get(symbol.lexical), result.get(symbol.datatype), result.get(symbol.lang)); } else { throw new IllegalArgumentException("Found no node for id " + id); } } }; private final Function<Long, NODE> cachingNodeTransformer = new Function<Long, NODE>() { @Override public NODE apply(Long input) { return context.getNode(input, nodeTransformer); } }; public RDBConnection(RDBContext context) { this.context = context; this.defaultDatatypeId = getId(new UID("default:default")); this.defaultLocaleId = getLangId(new Locale("")); } private void addLocale(Integer id, Locale locale) { SQLMergeClause merge = context.createMerge(language); merge.keys(language.id); merge.set(language.id, id); merge.set(language.text, LocaleUtil.toLang(locale)); merge.execute(); } private void addLocales(List<Integer> ids, List<Locale> locales) { Set<Integer> persisted = new HashSet<Integer>(context.createQuery() .from(language) .where(language.id.in(ids)) .list(language.id)); for (int i = 0; i < ids.size(); i++) { Integer id = ids.get(i); if (!persisted.contains(id)) { addLocale(id, locales.get(i)); } } ids.clear(); locales.clear(); } public void addLocales(Set<Locale> l, @Nullable BiMap<Locale, Integer> cache) { List<Integer> ids = new ArrayList<Integer>(ADD_BATCH); List<Locale> locales = new ArrayList<Locale>(ADD_BATCH); for (Locale locale : l) { Integer id = context.getLangId(locale); ids.add(id); locales.add(locale); if (cache != null) { cache.put(locale, id); } if (ids.size() == ADD_BATCH) { addLocales(ids, locales); } } if (!ids.isEmpty()) { addLocales(ids, locales); } } private void addNodes(List<Long> ids, List<NODE> nodes) { Set<Long> persisted = new HashSet<Long>(context.createQuery() .from(symbol) .where(symbol.id.in(ids)) .list(symbol.id)); if (persisted.size() < ids.size()) { SQLMergeClause merge = context.createMerge(symbol); for (int i = 0; i < ids.size(); i++) { Long id = ids.get(i); if (!persisted.contains(id)) { populate(merge, symbol, id, nodes.get(i)).addBatch(); } } merge.execute(); } ids.clear(); nodes.clear(); } public void addNodes(Set<NODE> n, @Nullable BiMap<NODE, Long> cache) { List<Long> ids = new ArrayList<Long>(ADD_BATCH); List<NODE> nodes = new ArrayList<NODE>(ADD_BATCH); for (NODE node : n) { Long nodeId = getId(node); ids.add(nodeId); nodes.add(node); if (cache != null) { cache.put(node, nodeId); } if (ids.size() == ADD_BATCH) { addNodes(ids, nodes); } } if (!ids.isEmpty()) { addNodes(ids, nodes); } } @Override public RDFBeanTransaction beginTransaction(boolean readOnly, int txTimeout, int isolationLevel) { return context.beginTransaction(readOnly, txTimeout, isolationLevel); } @Override public void clear() { context.clear(); } @Override public void close() { context.close(); } @Override public BID createBNode() { return new BID(UUID.randomUUID().toString()); } @Override public <D, Q> Q createUpdate(UpdateLanguage<D, Q> updateLanguage, D definition) { throw new UnsupportedOperationException(updateLanguage.toString()); } @SuppressWarnings("unchecked") @Override public <D, Q> Q createQuery(QueryLanguage<D, Q> queryLanguage, D definition) { if (queryLanguage.equals(QueryLanguage.TUPLE) || queryLanguage.equals(QueryLanguage.BOOLEAN) || queryLanguage.equals(QueryLanguage.GRAPH)) { RDBRDFVisitor visitor = new RDBRDFVisitor(context, cachingNodeTransformer); return (Q) visitor.visit((QueryMetadata) definition, queryLanguage); } else { throw new UnsupportedOperationException(); } } public void deleteFromContext(UID model) { SQLDeleteClause delete = context.createDelete(statement); delete.where(statement.model.eq(getId(model))); delete.execute(); } public List<STMT> find( @Nullable ID subject, @Nullable UID predicate, @Nullable NODE object, @Nullable UID model, boolean includeInferred) { return IteratorAdapter.asList(findStatements(subject, predicate, object, model, includeInferred)); } @Override @SuppressWarnings("serial") public CloseableIterator<STMT> findStatements( @Nullable final ID subject, @Nullable final UID predicate, @Nullable final NODE object, @Nullable final UID model, boolean includeInferred) { SQLQuery query = this.context.createQuery(); query.from(statement); final List<Expression<?>> exprs = new ArrayList<Expression<?>>(); if (subject != null) { query.where(statement.subject.eq(getId(subject))); } else { query.innerJoin(statement.subjectFk, sub); exprs.add(sub.lexical); } if (predicate != null) { query.where(statement.predicate.eq(getId(predicate))); } else { exprs.add(statement.predicate); } if (object != null) { query.where(statement.object.eq(getId(object))); } else { query.innerJoin(statement.objectFk, obj); exprs.add(obj.resource); exprs.add(obj.lexical); exprs.add(obj.datatype); exprs.add(obj.lang); } if (model != null) { query.where(statement.model.eq(getId(model))); } else { exprs.add(statement.model); } // return ordered result, if all triples are queried if (subject == null && predicate == null && object == null && model == null) { query.orderBy(statement.model.asc()); query.orderBy(statement.subject.asc()); query.orderBy(statement.predicate.asc()); query.orderBy(statement.object.asc()); } // add dummy projection if none is specified if (exprs.isEmpty()) { exprs.add(NumberTemplate.ONE); } Expression<STMT> stmt = new FactoryExpression<STMT>() { @Override public STMT newInstance(Object... args) { ID s = subject; UID p = predicate; NODE o = object; UID m = model; int counter = 0; if (s == null) { s = getNode(true, (String) args[counter++], null, null).asResource(); } if (p == null) { p = getNode((Long) args[counter++]).asURI(); } if (o == null) { o = getNode((Boolean) args[counter++], (String) args[counter++], (Long) args[counter++], (Integer) args[counter++]); } if (m == null && args[counter] != null && !args[counter].equals(Long.valueOf(0l))) { m = getNode((Long) args[counter]).asURI(); if (m.equals(RDB.nullContext)) { m = null; } } return new STMT(s, p, o, m); } @Override public List<Expression<?>> getArgs() { return exprs; } @Override public <R, C> R accept(Visitor<R, C> v, C context) { return v.visit(this, context); } @Override public Class<? extends STMT> getType() { return STMT.class; } }; return query.iterate(stmt); } @Override public boolean exists(ID subject, UID predicate, NODE object, UID context, boolean includeInferred) { CloseableIterator<STMT> iter = findStatements(subject, predicate, object, context, includeInferred); try { return iter.hasNext(); } finally { iter.close(); } } private Long getId(NODE node) { return context.getNodeId(node); } @Nullable private Integer getLangId(@Nullable Locale lang) { if (lang == null) { return null; } else { return context.getLangId(lang); } } private Locale getLocale(int id) { return context.getLang(id); } @Override public long getNextLocalId() { return context.getNextLocalId(); } private NODE getNode(boolean res, String lex, Long datatype, Integer lang) { if (res) { return context.getID(lex); } else { if (lang != null && !lang.equals(defaultLocaleId)) { return new LIT(lex, getLocale(lang)); } else if (datatype != null && !datatype.equals(defaultDatatypeId)) { return new LIT(lex, getNode(datatype).asURI()); } else { return new LIT(lex); } } } private NODE getNode(long id) { return context.getNode(id, nodeTransformer); } private <C extends StoreClause<C>> C populate(C clause, QStatement statement, STMT stmt) { Long c = stmt.getContext() != null ? getId(stmt.getContext()) : getId(RDB.nullContext); Long s = getId(stmt.getSubject()); Long p = getId(stmt.getPredicate()); Long o = getId(stmt.getObject()); clause.set(statement.model, c); clause.set(statement.subject, s); clause.set(statement.predicate, p); clause.set(statement.object, o); return clause; } private <C extends DeleteClause<C>> C populate(C clause, QStatement statement, STMT stmt) { Long c = stmt.getContext() != null ? getId(stmt.getContext()) : getId(RDB.nullContext); Long s = getId(stmt.getSubject()); Long p = getId(stmt.getPredicate()); Long o = getId(stmt.getObject()); clause.where(statement.model.eq(c)); clause.where(statement.subject.eq(s)); clause.where(statement.predicate.eq(p)); clause.where(statement.object.eq(o)); return clause; } private <C extends StoreClause<C>> C populate(C clause, QSymbol symbol, Long nodeId, NODE node) { long datatypeId = defaultDatatypeId; int langId = defaultLocaleId; double floatVal = 0.0; Timestamp datetimeVal = DEFAULT_TIMESTAMP; clause.set(symbol.id, nodeId); clause.set(symbol.resource, node.isResource()); clause.set(symbol.lexical, node.getValue()); if (node.isLiteral()) { LIT literal = node.asLiteral(); datatypeId = getId(literal.getDatatype()); if (literal.getLang() != null) { langId = getLangId(literal.getLang()); } else if (Constants.integerTypes.contains(literal.getDatatype())) { floatVal = Double.valueOf(literal.getValue()); } else if (Constants.decimalTypes.contains(literal.getDatatype())) { floatVal = Double.valueOf(literal.getValue()); } else if (Constants.dateTypes.contains(literal.getDatatype())) { datetimeVal = new Timestamp(context.convert(literal.getValue(), java.sql.Date.class).getTime()); floatVal = datetimeVal.getTime(); } else if (Constants.dateTimeTypes.contains(literal.getDatatype())) { datetimeVal = context.convert(literal.getValue(), Timestamp.class); floatVal = datetimeVal.getTime(); } } clause.set(symbol.datatype, datatypeId); clause.set(symbol.lang, langId); clause.set(symbol.floatval, floatVal); clause.set(symbol.datetimeval, datetimeVal); return clause; } @Override public void remove(ID s, UID p, NODE o, UID c) { SQLDeleteClause delete = context.createDelete(statement); if (s != null) { delete.where(statement.subject.eq(getId(s))); } if (p != null) { delete.where(statement.predicate.eq(getId(p))); } if (o != null) { delete.where(statement.object.eq(getId(o))); } if (c != null) { delete.where(statement.model.eq(getId(c))); } delete.execute(); } @Override public void update(Collection<STMT> removedStatements, Collection<STMT> addedStatements) { // remove Set<NODE> oldNodes = new HashSet<NODE>(); if (removedStatements != null) { for (STMT stmt : removedStatements) { if (stmt.getContext() != null) { oldNodes.add(stmt.getContext()); } oldNodes.add(stmt.getSubject()); oldNodes.add(stmt.getPredicate()); oldNodes.add(stmt.getObject()); } if (!removedStatements.isEmpty()) { Iterator<STMT> stmts = removedStatements.iterator(); SQLDeleteClause delete = context.createDelete(statement); populate(delete, statement, stmts.next()).addBatch(); int counter = 1; while (stmts.hasNext()) { counter++; populate(delete, statement, stmts.next()).addBatch(); if (counter == DELETE_BATCH) { delete.execute(); delete = context.createDelete(statement); counter = 0; } } if (counter > 0) { delete.execute(); } } } // insert Set<NODE> newNodes = new HashSet<NODE>(); if (addedStatements != null) { for (STMT stmt : addedStatements) { if (stmt.getContext() != null) { newNodes.add(stmt.getContext()); } newNodes.add(stmt.getSubject()); newNodes.add(stmt.getPredicate()); newNodes.add(stmt.getObject()); if (stmt.getObject().isLiteral()) { LIT lit = stmt.getObject().asLiteral(); if (lit.getDatatype() != null) { newNodes.add(lit.getDatatype()); } } } } // insert nodes newNodes.removeAll(oldNodes); newNodes.removeAll(context.getNodes()); addNodes(newNodes, null); // insert stmts if (addedStatements != null && !addedStatements.isEmpty()) { Iterator<STMT> stmts = addedStatements.iterator(); SQLMergeClause merge = context.createMerge(statement); populate(merge, statement, stmts.next()).addBatch(); int counter = 1; while (stmts.hasNext()) { counter++; populate(merge, statement, stmts.next()).addBatch(); if (counter == ADD_BATCH) { merge.execute(); merge = context.createMerge(statement); counter = 0; } } if (counter > 0) { merge.execute(); } } } @Override public QueryOptions getQueryOptions() { return QueryOptions.ALL; } @Override public InferenceOptions getInferenceOptions() { return InferenceOptions.DEFAULT; } }