/******************************************************************************* * Copyright 2014 Miami-Dade County * * 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.sharegov.cirm.user; import java.util.Map; import mjson.Json; import org.semanticweb.owlapi.model.IRI; import org.semanticweb.owlapi.model.OWLDataProperty; import org.semanticweb.owlapi.model.OWLObjectProperty; import org.sharegov.cirm.OWL; import org.sharegov.cirm.rest.OWLIndividuals; import org.sharegov.cirm.utils.JsonUtil; import org.sharegov.cirm.utils.ThreadLocalStopwatch; /** * OntoUserProvider retrieves mdc:User individuals from the ontologies. <br> * Users are managed in mdc: ontology * <br> * A mdc:User is modelled as follows: * OWLClass: mdc:User or Subclass (==DEFAULT_IRI_BASE) * <br> * User IRI: mdc:cKey/eKey OR mdc:emailAddress (In cases where no cKey is available, in such case the User is also an mdc:EmailAddress) <br> * (The User IRI fragment will be user in the assignedTo field of activities; either cKey/eKey or Email)<br> * User Email: mdc:hasEmailAddress mdc:EmailAddress individual mandatory <br> * (If no cKey/eKey, User individual A has two Classes: mdc:User and mdc:EmailAddress, the mdc:hasEmailAddress property refers to A) <br> * User FirstName: mdc:FirstName (xsd:String) mandatory <br> * User LastName: mdc:LastName (xsd:String) mandatory <br> * User MiddleName: mdc:MiddleName (xsd:String) OPTIONAL <br> * User PhoneNumber: mdc:PhoneNumber (xsd:String) OPTIONAL <br> * <br> * Find on String attributes is never case sensitive. * <br> * This object contains an internal cache of users. Do not instantiate this object for each request, but use a Ref instead.<br> * If User ontology users change, create one new OntoUserProvider. * * @author Thomas Hilpold */ public class OntoUserProvider implements UserProvider { /** * This WildCard is allowed only at the end of an attibute value. */ public static final String SEARCH_STARTS_WITH_WILDCARD = "%"; public static final String ID_ATTRIBUTE = "iri"; public static final OWLDataProperty USER_FIRSTNAME_DP = OWL.dataProperty(OWL.fullIri("mdc:FirstName")); public static final OWLDataProperty USER_LASTNAME_DP = OWL.dataProperty(OWL.fullIri("mdc:LastName")); public static final OWLDataProperty USER_PHONE_NUMBER_DP_OPT= OWL.dataProperty(OWL.fullIri("mdc:PhoneNumber")); public static final OWLDataProperty USER_MIDDLENAME_DP_OPT = OWL.dataProperty(OWL.fullIri("mdc:MiddleName")); public static final OWLObjectProperty USER_HAS_EMAIL_ADDRESS_OP = OWL.objectProperty(OWL.fullIri("mdc:hasEmailAddress")); private volatile Json users = null; public OntoUserProvider() { ensureUsers(); } public boolean authenticate(String username, String password) { boolean result = false; return result; } @Override public Json find(String attribute, String value) { Json prototype = Json.object(); prototype.set(attribute, value); return find(prototype); } @Override public Json find(Json prototype) { return find(prototype, Integer.MAX_VALUE); } public Json findGroups(String id) { return Json.array(); } /** * Finds all users matching the prototype. * Use wildcard as last char of a search value on string attribute values only. * Current implementation scans over user list to find matches. */ @Override public Json find(Json prototype, int resultLimit) { Json result = Json.array(); int matchCount = 0; for (Json candidate : users.asJsonList()) { if (match(candidate, prototype)) { result.add(candidate); matchCount ++; } if (matchCount >= resultLimit) break; } return result; } /** * Determines if all attribute values in search are found in canditate (not case sensitive, wildcard allowed on String val attributes only). * @param candidate * @param search * @return */ public boolean match(Json candidate, Json search) { if (search.asJsonMap().isEmpty()) return true; for (Map.Entry<String, Json> searchE : search.asJsonMap().entrySet()) { if (!candidate.asJsonMap().containsKey(searchE.getKey()) || !matchValue(candidate.at(searchE.getKey()), searchE.getValue())) return false; } return true; } public boolean matchValue(Object candidateValue, Object searchValue) { boolean result; if (!(searchValue instanceof String)) { //Might be int, double or other search, no wild card result = searchValue.equals(candidateValue); } else { String candVal = candidateValue.toString().trim().toUpperCase(); String searchVal = searchValue.toString().trim().toUpperCase(); if (searchVal.lastIndexOf(SEARCH_STARTS_WITH_WILDCARD) == searchVal.length() - 1) { //Remove WildCard searchVal = searchVal.substring(0, searchVal.length() - 1); result = candVal.startsWith(searchVal); } else result = candVal.equals(searchVal); } return result; } /** * @param id null returns empty map * @return the user or null */ @Override public Json get(String id) { if (id == null) return null; ensureUsers(); for (Json user: users.asJsonList()) { String compareID = user.at("hasUsername").asString(); if (id.toUpperCase().equals(compareID.toUpperCase())) return user; } return Json.nil(); } /** * Gets a map for a user Json with iris for each known attribute value resolved to fragments. * @param user * @return null, if a user has no email (logged) */ private Json massageUser(Json user) { String userId = user.at(ID_ATTRIBUTE).asString(); Json userEmailMapOrString = user.at(USER_HAS_EMAIL_ADDRESS_OP.getIRI().getFragment()); if (userEmailMapOrString == null) { ThreadLocalStopwatch.getWatch().time("OntoUserProvider: Ill configured user: " + userId + " Check ontology and make sure it has all mandatory attributes."); return null; } user.set(ID_ATTRIBUTE, IRI.create(userId).getFragment()); //Email Address String userEmailValue; if (userEmailMapOrString.isObject()) { String userEmailIri = userEmailMapOrString.at("iri").asString(); userEmailValue = IRI.create(userEmailIri).getFragment(); } else { userEmailValue = userEmailMapOrString.asString(); } user.set(USER_HAS_EMAIL_ADDRESS_OP.getIRI().getFragment(), userEmailValue); return user.set("hasUsername", user.at(ID_ATTRIBUTE)) .set("email", user.at("hasEmailAddress")); } @Override public String getIdAttribute() { return ID_ATTRIBUTE; } private void ensureUsers() { if (users == null) { synchronized (this) { if (users == null) users = retrieveMappedUsers(); } } } /** * Retrieves all Users from the ontologies * using reasoner first, then OwlEntityCache * @return */ private Json retrieveUsers() { Json result; ThreadLocalStopwatch.getWatch().time("OntoUserProvider: retrieveUsers"); result = JsonUtil.ensureArray(new OWLIndividuals().doQuery("mdc:User")); ThreadLocalStopwatch.getWatch().time("OntoUserProvider: retrieveUsers completed: nrOfUsers: " + result.asJsonList().size()); return result; } /** * Retrieves users as Map<String, Object>, prepared for find operations. * All Object property values and iris of known attributes are resolved to their iri fragments. * @return */ private Json retrieveMappedUsers() { try { Json users = retrieveUsers(); Json result = Json.array(); for (Json user: users.asJsonList()) { user = massageUser(user); if (user != null) result.add(user); } return result; } catch (Exception e) { ThreadLocalStopwatch.getWatch().time("OntoUserProvider: Error during retrieveMappedUsers from ontology. Empty user list will now be used. " + e.toString()); e.printStackTrace(); return Json.array(); } } public Json populate(Json user) { if (user.has("userid")) { Json found = get(user.at("userid").asString()); if (!found.isNull()) { user.set("onto", found); JsonUtil.setIfMissing(user, "email", found.at("hasEmailAddress")); JsonUtil.setIfMissing(user, "FirstName", found.at("FirstName")); JsonUtil.setIfMissing(user, "LastName", found.at("LastName")); JsonUtil.setIfMissing(user, "hasUsername", found.at("hasUsername")); } } return user; } }