/**
* $Id: EntityTaggingService.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $
* $URL: https://source.sakaiproject.org/svn/entitybroker/trunk/impl/src/java/org/sakaiproject/entitybroker/impl/EntityTaggingService.java $
* EntityTaggingService.java - entity-broker - Aug 4, 2008 10:37:25 AM - azeckoski
**************************************************************************
* Copyright (c) 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.entitybroker.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import org.sakaiproject.entitybroker.EntityBrokerManager;
import org.sakaiproject.entitybroker.EntityReference;
import org.sakaiproject.entitybroker.EntityView;
import org.sakaiproject.entitybroker.dao.EntityBrokerDao;
import org.sakaiproject.entitybroker.dao.EntityTagApplication;
import org.sakaiproject.entitybroker.entityprovider.EntityProviderManager;
import org.sakaiproject.entitybroker.entityprovider.capabilities.TagProvideable;
import org.sakaiproject.entitybroker.entityprovider.capabilities.Taggable;
import org.sakaiproject.entitybroker.entityprovider.extension.EntityData;
import org.sakaiproject.entitybroker.entityprovider.extension.TagSearchService;
import org.sakaiproject.entitybroker.util.TemplateParseUtil;
import org.sakaiproject.genericdao.api.search.Order;
import org.sakaiproject.genericdao.api.search.Restriction;
import org.sakaiproject.genericdao.api.search.Search;
/**
* Handles calls through the system related to tagging,
* can delegate to a central tagging service
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
public class EntityTaggingService implements TagSearchService {
private EntityBrokerDao dao;
public void setDao(EntityBrokerDao dao) {
this.dao = dao;
}
private EntityProviderManager entityProviderManager;
public void setEntityProviderManager(EntityProviderManager entityProviderManager) {
this.entityProviderManager = entityProviderManager;
}
private EntityBrokerManager entityBrokerManager;
public void setEntityBrokerManager(EntityBrokerManager entityBrokerManager) {
this.entityBrokerManager = entityBrokerManager;
}
public List<EntityData> findEntitesByTags(String[] tags, String[] prefixes,
boolean matchAll, org.sakaiproject.entitybroker.entityprovider.search.Search search) {
// FIXME the match all and handling of merging results from multiple providers is currently a mess -AZ
// check for valid inputs
if (tags == null || tags.length == 0) {
throw new IllegalArgumentException(
"At least one tag must be supplied to this search, tags cannot be null or empty");
}
List<EntityData> results = new ArrayList<EntityData>();
// do a quick check first for efficiency, only search by prefix if there is a valid prefix to check
boolean doSearch = true;
HashSet<String> validPrefixes = new HashSet<String>();
if (prefixes != null) {
for (String prefix : prefixes) {
boolean valid = true;
if (entityProviderManager.getProviderByPrefix(prefix) == null) {
valid = false;
} else {
if (entityProviderManager.getProviderByPrefixAndCapability(prefix, Taggable.class) == null) {
valid = false;
}
}
if (valid) {
validPrefixes.add(prefix);
}
}
if (validPrefixes.size() == 0) {
doSearch = false;
}
}
if (doSearch) {
// search the internal storage first
Search dbSearch = new Search();
if (!matchAll && search != null) {
// cannot use limits with match all...
dbSearch.setLimit(search.getLimit());
dbSearch.setStart(search.getStart());
}
dbSearch.addRestriction( new Restriction("tag", tags) );
if (validPrefixes.size() > 0) {
dbSearch.addRestriction( new Restriction("entityPrefix", validPrefixes.toArray(new String[validPrefixes.size()])) );
}
dbSearch.addOrder( new Order("entityRef") );
List<EntityTagApplication> tagApps = dao.findBySearch(EntityTagApplication.class, dbSearch);
if (matchAll) {
// handle match all
HashMap<String, Integer> matchMap = new HashMap<String, Integer>();
for (EntityTagApplication tagApp : tagApps) {
if (matchMap.containsKey(tagApp.getEntityRef())) {
int current = matchMap.get(tagApp.getEntityRef()).intValue();
matchMap.put(tagApp.getEntityRef(), current + 1);
} else {
matchMap.put(tagApp.getEntityRef(), 1);
}
}
int allCount = tags.length;
for (Entry<String, Integer> entry : matchMap.entrySet()) {
if (entry.getValue() >= allCount) {
results.add( new EntityData(entry.getKey(), (String)null) );
}
}
Collections.sort(results, new EntityData.ReferenceComparator());
} else {
// filter the list down to the references first
HashMap<String, String> refToTags = new HashMap<String, String>();
for (EntityTagApplication tagApp : tagApps) {
if (refToTags.containsKey(tagApp.getEntityRef())) {
refToTags.put(tagApp.getEntityRef(), refToTags.get(tagApp.getEntityRef()) + EntityView.SEPARATOR + tagApp.getTag());
} else {
refToTags.put(tagApp.getEntityRef(), tagApp.getTag());
// note: no display available here
results.add( new EntityData(tagApp.getEntityRef(), (String)null) );
}
}
// add in the tags property
for (EntityData ed : results) {
String reference = ed.getEntityReference().toString();
ed.getEntityProperties().put("tags", refToTags.get(reference));
}
}
if (isUnderSearchLimit(results.size(), search)) {
// TODO how to handle search.start for these?
// get the results from any entity providers which supply tag search results
List<TagProvideable> tagProviders = entityProviderManager.getProvidersByCapability(TagProvideable.class);
for (TagProvideable provider : tagProviders) {
// only call if prefixes match and we are not at the limit
if (isUnderSearchLimit(results.size(), search)) {
if (validPrefixes.size() == 0
|| validPrefixes.contains(provider.getEntityPrefix())) {
List<EntityData> pList = provider.findEntitesByTags(tags, matchAll, search);
if (pList != null) {
for (EntityData esr : pList) {
if (isUnderSearchLimit(results.size(), search)) {
results.add( esr );
} else {
break;
}
}
}
}
}
}
// TODO fix up the order?
}
// populate the entity search results URLs (display names?)
entityBrokerManager.populateEntityData(results);
}
return results;
}
private boolean isUnderSearchLimit(int value, org.sakaiproject.entitybroker.entityprovider.search.Search search) {
boolean under = false;
if (search == null) {
under = true;
} else {
if (search.getLimit() > 0) {
if (value < search.getLimit()) {
under = true;
}
} else {
under = true;
}
}
return under;
}
public List<String> getTagsForEntity(String reference) {
ArrayList<String> tags = new ArrayList<String>();
EntityReference ref = entityBrokerManager.parseReference(reference);
if (ref == null) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), no entity provider to handle this reference");
} else {
reference = ref.toString();
if (! entityBrokerManager.entityExists(ref)) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), entity does not exist");
}
if (entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), Taggable.class) != null) {
TagProvideable provider = entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), TagProvideable.class);
if (provider == null) {
// put in call to central tag system here if desired
List<EntityTagApplication> results = dao.findBySearch(EntityTagApplication.class, new Search("entityRef", reference));
for (EntityTagApplication entityTagApplication : results) {
tags.add(entityTagApplication.getTag());
}
} else {
List<String> tList = provider.getTagsForEntity(reference);
if (tList != null) {
tags.addAll( tList );
}
}
} else {
throw new UnsupportedOperationException("Cannot get tags from this entity ("+reference+"), it has no support for tagging in its entity provider");
}
}
Collections.sort(tags);
return tags;
}
public void addTagsToEntity(String reference, String[] tags) {
if (tags == null) {
throw new IllegalArgumentException("Invalid params, tags cannot be null");
}
validateTags(tags); // ensures tags are valid
EntityReference ref = entityBrokerManager.parseReference(reference);
if (ref == null) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), no entity provider to handle this reference");
} else {
reference = ref.toString();
if (! entityBrokerManager.entityExists(ref)) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), entity does not exist");
}
if (entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), Taggable.class) != null) {
TagProvideable provider = entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), TagProvideable.class);
if (provider == null) {
// put in call to central tag system here if desired
Set<String> addTags = new HashSet<String>();
Set<String> removeTags = new HashSet<String>();
diffEntityTags(reference, tags, addTags, removeTags);
Set<EntityTagApplication> newTagApps = new HashSet<EntityTagApplication>();
for (String tag : addTags) {
newTagApps.add( new EntityTagApplication(reference, ref.getPrefix(), tag) );
}
if (newTagApps.size() > 0) {
dao.saveSet(newTagApps);
}
} else {
provider.addTagsToEntity(reference, tags);
}
} else {
throw new UnsupportedOperationException("Cannot set tags for this entity ("+reference+"), it has no support for tagging in its entity provider");
}
}
}
public void removeTagsFromEntity(String reference, String[] tags) {
if (tags == null) {
throw new IllegalArgumentException("Invalid params, tags cannot be null");
}
validateTags(tags); // ensures tags are valid
EntityReference ref = entityBrokerManager.parseReference(reference);
if (ref == null) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), no entity provider to handle this reference");
} else {
reference = ref.toString();
if (! entityBrokerManager.entityExists(ref)) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), entity does not exist");
}
if (entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), Taggable.class) != null) {
TagProvideable provider = entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), TagProvideable.class);
if (provider == null) {
// put in call to central tag system here if desired
dao.deleteTags(reference, tags);
} else {
provider.removeTagsFromEntity(reference, tags);
}
} else {
throw new UnsupportedOperationException("Cannot set tags for this entity ("+reference+"), it has no support for tagging in its entity provider");
}
}
}
public void setTagsForEntity(String reference, String[] tags) {
if (tags == null) {
throw new IllegalArgumentException("Invalid params, tags cannot be null");
}
validateTags(tags); // ensures tags are valid
EntityReference ref = entityBrokerManager.parseReference(reference);
if (ref == null) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), no entity provider to handle this reference");
} else {
reference = ref.toString();
if (! entityBrokerManager.entityExists(ref)) {
throw new IllegalArgumentException("Invalid reference (" + reference + "), entity does not exist");
}
if (entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), Taggable.class) != null) {
TagProvideable provider = entityProviderManager.getProviderByPrefixAndCapability(ref.getPrefix(), TagProvideable.class);
if (provider == null) {
// put in call to central tag system here if desired
Set<String> addTags = new HashSet<String>();
Set<String> removeTags = new HashSet<String>();
diffEntityTags(reference, tags, addTags, removeTags);
Set<EntityTagApplication> newTagApps = new HashSet<EntityTagApplication>();
for (String tag : addTags) {
newTagApps.add( new EntityTagApplication(reference, ref.getPrefix(), tag) );
}
if (newTagApps.size() > 0) {
dao.saveSet(newTagApps);
}
if (removeTags.size() > 0) {
dao.deleteTags(reference, removeTags.toArray(new String[removeTags.size()]));
}
} else {
provider.setTagsForEntity(reference, tags);
}
} else {
throw new UnsupportedOperationException("Cannot set tags for this entity ("+reference+"), it has no support for tagging in its entity provider");
}
}
}
/**
* Finds the current tags and then diffs them to get the tags which need to be added and the tags which need to be deleted
* @param reference entity ref
* @param tags desired set of tags
* @param addTags tags which need to be added
* @param removeTags tags which need to be removed
*/
protected void diffEntityTags(String reference, String[] tags, Set<String> addTags, Set<String> removeTags) {
// first get the current set of tags for this reference
Set<String> curTags = new HashSet<String>();
List<EntityTagApplication> results = dao.findBySearch(EntityTagApplication.class, new Search("entityRef", reference));
for (EntityTagApplication entityTagApplication : results) {
curTags.add(entityTagApplication.getTag());
}
Set<String> setTags = new HashSet<String>();
for (String tag : tags) {
setTags.add(tag);
if (curTags.contains(tag)) {
continue;
} else {
addTags.add(tag);
}
}
curTags.removeAll(setTags);
removeTags.clear();
removeTags.addAll(curTags);
}
/**
* Validate a set of tags
* @param tags
*/
protected void validateTags(String[] tags) {
for (String tag : tags) {
if (! tag.matches(TemplateParseUtil.VALID_VAR_CHARS+"+")) {
throw new IllegalArgumentException("Invalid tag ("+tag+"), tags can only contain the following (not counting []): " + TemplateParseUtil.VALID_VAR_CHARS);
}
}
}
}