/*
* (c) Copyright 2006 Hewlett-Packard Development Company, LP
* All rights reserved.
* [See end of file]
*/
package com.hp.hpl.squirrelrdf.ldap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.hp.hpl.jena.graph.Node;
import com.hp.hpl.jena.graph.Triple;
import com.hp.hpl.jena.graph.impl.LiteralLabel;
import com.hp.hpl.jena.query.core.Binding;
import com.hp.hpl.jena.query.engine1.ExecutionContext;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.ResIterator;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.vocabulary.RDF;
import com.hp.hpl.squirrelrdf.querymap.QueryMapEngine;
/**
* The meat of the mapping. Takes a triple pattern (with same subject), maps to
* an ldap query, and maps that result to rdf node values.
*
* @author pldms
*
*/
public class LdapSubQuery implements QueryMapEngine
{
private static final Log log = LogFactory.getLog(LdapSubQuery.class);
private Node subject;
private List<Triple> query;
private Model config;
private Resource server;
private Resource base;
private Node type;
public LdapSubQuery(Model config)
{
this.subject = null;
this.query = new ArrayList<Triple>();
this.config = config;
this.base = getSubject(RDF.type, LdapMap.Map).getProperty(
LdapMap.server).getResource();
this.server = getServer(base);
this.type = null;
}
/* Get the server part from the base */
private Resource getServer(Resource base2)
{
String server = base2.getURI().substring(0, base2.getURI().lastIndexOf("/") + 1);
return base2.getModel().createResource(server);
}
/**
* Add a triple. All triples should have the same subject.
*
* @param triple
*/
public void add(Triple triple)
{
if (subject == null)
subject = triple.getSubject();
query.add(triple);
}
/**
* Get the subject for this pattern.
* @return The subject
*/
public Node getSubject()
{
return subject;
}
/**
* Get the pattern as a list of triples
* @return Query pattern
*/
public List<Triple> getTriples()
{
return query;
}
/*
* Execute this (sub)query, given an exisiting binding.
*
* The bulk of this is concerned with mapping the ldap results to rdf, and
* checking whether the binding is consistent. Since we prebind this is a
* little over the top (though cheap). We could also check for cross
* products and cache results.
*
* (non-Javadoc)
*
* @see com.hp.hpl.eim.ldap.QueryMapEngine#execute(com.hp.hpl.jena.query.core.Binding)
*/
public Iterator<Map<String, Node>> execute(Binding binding, ExecutionContext context)
{
List<Map<String, Node>> result = new ArrayList<Map<String, Node>>();
NamingEnumeration<SearchResult> ldapResult = null;
try
{
ldapResult = executeLdap(binding, context);
while (ldapResult.hasMore())
{
SearchResult sr = ldapResult.next();
Attributes sra = sr.getAttributes();
Map<String, Node> aBinding = new HashMap<String, Node>();
// Mapping results to rdf
if (subject.isVariable())
aBinding.put(subject.getName(), createLdapNode(sr
.getNameInNamespace()));
boolean valid = true;
for (Triple triple : query)
{
if (!triple.getObject().isVariable())
continue;
String attribute = predToAttribute(triple.getPredicate());
if (sra.get(attribute) == null) // no value -- not a hit
{
log.debug("Attribute not found");
valid = false;
break;
}
// FIXME I get the first value, but there may be more. tricky.
// TODO If pred is type unmap value to node (often multiple vals, btw)
Object val = sra.get(attribute).get();
String var = triple.getObject().getName();
// I don't think this does anything useful, since we prebind
if (aBinding.containsKey(var)) // if we have a binding for
// this
{
if (!aBinding.get(var).equals(val)) // and it differs,
// not a consistent
// binding
{
valid = false;
break;
}
continue; // All is well, keep what we have
}
Node nodeVal;
Resource valType = getObjectType(triple.getPredicate());
// This attribute points to a ldap node
if (LdapMap.ObjectProperty.equals(valType))
nodeVal = createLdapNode((String) val);
else if (LdapMap.EmailProperty.equals(valType))
nodeVal = Node.createURI("mailto:" + val);
else if (LdapMap.URIProperty.equals(valType))
nodeVal = Node.createURI(val.toString());
else if (val instanceof String) // avoid xsd:string
nodeVal = Node.createLiteral((String) val);
else
{
LiteralLabel ll = new LiteralLabel(val);
nodeVal = Node.createLiteral(ll);
}
aBinding.put(var, nodeVal);
}
if (valid)
result.add(aBinding);
}
ldapResult.close();
}
catch (NamingException e) // Flee, all is lost...
{
throw new RuntimeException("Ldap problem", e);
}
return result.iterator();
}
/**
* Create an ldap:// uri node from a basename. Normalise a little.
*
*/
private Node createLdapNode(String val)
{
String toReturn = server.getURI()
+ val.toLowerCase().replaceAll(" ", "%20");
return Node.createURI(toReturn);
}
/**
* Execute the query.
*
* This takes a subquery, creates, and executes an ldap query, prebinding
* variables if possible.
*
* @param binding
* @return LDAP result
* @throws NamingException
*/
public NamingEnumeration<SearchResult> executeLdap(Binding binding, ExecutionContext econtext)
throws NamingException
{
if (query.isEmpty())
{
log.warn("(sub)Query is empty");
return null;
}
Collection<String> toGet = new ArrayList<String>();
Collection<String> toMatch = new ArrayList<String>();
/* Prebind subject (if possible) */
Node theSubject = subject;
if (subject.isVariable() && binding.contains(subject.getName()))
theSubject = binding.get(subject.getName());
for (Triple triple : query)
{
String attr = predToAttribute(triple.getPredicate());
/* Prebind object (if possible) */
Node theObject = triple.getObject();
if (theObject.isVariable() && binding.contains(theObject.getName()))
theObject = binding.get(theObject.getName());
Resource valType = getObjectType(triple.getPredicate());
if (theObject.isVariable())
toGet.add(attr);
else // TODO if pred is type, map values to ldap equivalent
{
toGet.add(attr); /* TODO dump this! stupid! Prebinding work around */
String match = null;
if (LdapMap.ObjectProperty.equals(valType))
match = unldap(theObject.getURI());
else if (LdapMap.EmailProperty.equals(valType))
match = unmailto(theObject.getURI());
else if (theObject.isURI()) // This could occur even if type
// isn't given
match = theObject.getURI();
else
match = theObject.getLiteralLexicalForm();
log.info("attr: '" + attr + "' match: '" + match + "'");
/* TODO Escape 'match' properly */
toMatch.add("(" + attr + "=" + match + ")");
}
}
String queryString;
if (toMatch.size() > 1)
queryString = "(&" + join(toMatch, "") + ")";
else
queryString = join(toMatch, "");
String baseURI = null;
if (theSubject.isVariable())
baseURI = base.getURI();
else
{
if (!theSubject.isURI()
|| !theSubject.getURI().startsWith(server.getURI()))
{
throw new RuntimeException(
"Unanswerable query. Subject invalid: " + theSubject);
}
baseURI = theSubject.getURI();
}
log.info("Server: " + baseURI);
log.info("filter: " + queryString);
log.info("get attributes: " + join(toGet, ","));
DirContext context = new InitialDirContext();
NamingEnumeration<SearchResult> results = null;
if ("".equals(queryString)) // We aren't searching, just get attributes
{
Attributes attrs = context.getAttributes(baseURI, (String[]) toGet
.toArray(new String[] {}));
// Wrap this to fit this method's contract
SearchResult sr = new SearchResult(null, null, attrs);
sr.setNameInNamespace(uriToName(baseURI));
results = new SingletonNamingEnumeration<SearchResult>(sr);
}
else // An actual query
{
SearchControls sc = new SearchControls();
sc.setReturningAttributes(toGet.toArray(new String[] {}));
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
results = context.search(baseURI, queryString, sc);
}
return results;
}
private String unmailto(String uri)
{
if (uri.startsWith("mailto:"))
return uri.substring(7);
return uri;
}
private String unldap(String uri)
{
if (uri.startsWith(server.getURI()))
return uri.substring(server.getURI().length()).replaceAll("%20",
" ");
return uri;
}
private String uriToName(String uri)
{
return uri.substring(server.getURI().length());
}
private String join(Collection<String> toJoin, String joinString)
{
StringBuffer sb = new StringBuffer();
String js = null;
for (String joinElem : toJoin)
{
if (js != null)
sb.append(js);
sb.append(joinElem);
if (js == null)
js = joinString;
}
return sb.toString();
}
private String predToAttribute(Node predicate)
{
return getSubject(LdapMap.property, config.asRDFNode(predicate))
.getProperty(LdapMap.attribute).getLiteral().getString();
}
private Resource getSubject(Property property, RDFNode object)
{
ResIterator found = config.listSubjectsWithProperty(property, object);
if (!found.hasNext())
return null;
Resource foundSubject = found.nextResource();
found.close();
return foundSubject;
}
private Resource getObjectType(Node predicate)
{
Resource subj = getSubject(LdapMap.property, config
.asRDFNode(predicate));
if (subj == null)
return null;
Statement s = subj.getProperty(RDF.type);
if (s == null)
return null;
return s.getResource();
}
/**
*
* This is a simple implementation to keep executeLdap uniform. It is for
* cases where no search is perfomed.
*
* @author pldms
*
*/
class SingletonNamingEnumeration<T> implements NamingEnumeration<T>
{
boolean hasMore;
T element;
public SingletonNamingEnumeration(T element)
{
this.element = element;
hasMore = true;
}
public T next() throws NamingException
{
return nextElement();
}
public boolean hasMore() throws NamingException
{
return hasMore;
}
public void close() throws NamingException
{
}
public boolean hasMoreElements()
{
return hasMore;
}
public T nextElement()
{
if (hasMore)
{
hasMore = false;
return element;
}
return null;
}
}
}
/*
* (c) Copyright 2006 Hewlett-Packard Development Company, LP All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. The name of the author may not
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/