/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* 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 au.gov.ga.earthsci.intent;
import java.net.URI;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.gov.ga.earthsci.common.util.ExtensionPointHelper;
/**
* Description of {@link Intent} values to be matched. Can match against the
* Intent's action, categories, content type, expected return type, and/or the
* URI. Provides the ability to find a suitable handler for an Intent.
* <p/>
* The intent filtering is performed as follows:
* <ul>
* <li>Action matches if any of the given values match the Intent action, or if
* no actions are specified by this filter.</li>
* <li>Categories matches if all of the categories in the Intent match
* categories in this filter.</li>
* <li>Content type matches if any of the given values match the Intent content
* type, or both the Intent and the filter don't specify a content type.</li>
* <li>Returns result matches if the intent's expected return type is defined,
* and this filter returns a result.</li>
* <li>Return type matches if any of the given values match the Intent expected
* return type if defined, or if the filter defines no return types.</li>
* <li>Data scheme matches if any of the given values match the Intent URI's
* scheme.</li>
* <li>Data authority matches if any of the given values match the Intent URI's
* authority, and the data scheme has already matched.</li>
* <li>Data path matches if any of the given values match the Intent URI's path,
* and the data scheme and authority have already matched.</li>
* </ul>
* Data schemes, authorities, and paths can contain wildcards '*'.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class IntentFilter
{
private int priority = 0;
private final Set<String> actions = new HashSet<String>();
private final Set<String> categories = new HashSet<String>();
private final Set<IContentType> contentTypes = new HashSet<IContentType>();
private boolean returnsResult = false;
private final Set<Class<?>> returnTypes = new HashSet<Class<?>>();
private final Set<URIFilter> uriFilters = new HashSet<URIFilter>();
private Class<? extends IIntentHandler> handler;
private static final Logger logger = LoggerFactory.getLogger(IntentFilter.class);
private String label;
private String description;
private URL icon;
private boolean prompt = true;
public IntentFilter()
{
}
protected IntentFilter(IConfigurationElement element) throws ClassNotFoundException
{
addToSetFromElements(element, "action", "name", actions); //$NON-NLS-1$ //$NON-NLS-2$
addToSetFromElements(element, "category", "name", categories); //$NON-NLS-1$ //$NON-NLS-2$
addToContentTypeSetFromElements(element, "content-type", "id", contentTypes); //$NON-NLS-1$ //$NON-NLS-2$
addToClassSetFromElements(element, "return-type", "class", returnTypes); //$NON-NLS-1$ //$NON-NLS-2$
for (IConfigurationElement uri : element.getChildren("uri")) //$NON-NLS-1$
{
uriFilters.add(new URIFilter(uri));
}
@SuppressWarnings("unchecked")
Class<? extends IIntentHandler> handler =
(Class<? extends IIntentHandler>) ExtensionPointHelper.getClassForProperty(element, "class"); //$NON-NLS-1$
setHandler(handler);
int priority = ExtensionPointHelper.getIntegerForProperty(element, "priority", 0); //$NON-NLS-1$
setPriority(priority);
boolean returnsResult = ExtensionPointHelper.getBooleanForProperty(element, "returns-result", false); //$NON-NLS-1$
setReturnsResult(returnsResult);
label = element.getAttribute("label"); //$NON-NLS-1$
description = element.getAttribute("description"); //$NON-NLS-1$
icon = ExtensionPointHelper.getResourceURLForProperty(element, "icon32"); //$NON-NLS-1$
prompt = ExtensionPointHelper.getBooleanForProperty(element, "prompt", prompt); //$NON-NLS-1$
}
protected static void addToSetFromElements(IConfigurationElement element, String childrenName,
String attributeName, Set<String> set)
{
for (IConfigurationElement child : element.getChildren(childrenName))
{
String name = child.getAttribute(attributeName);
if (!isEmpty(name))
{
set.add(name);
}
}
}
private void addToContentTypeSetFromElements(IConfigurationElement element, String childrenName,
String attributeName, Set<IContentType> set)
{
for (IConfigurationElement child : element.getChildren(childrenName))
{
String value = child.getAttribute(attributeName);
if (!isEmpty(value))
{
if (value.indexOf('*') < 0)
{
IContentType contentType = Platform.getContentTypeManager().getContentType(value);
if (contentType != null)
{
set.add(contentType);
}
}
else
{
IContentType[] types = Platform.getContentTypeManager().getAllContentTypes();
String wildcardRegex = URIFilter.wildcardRegex(value);
for (IContentType type : types)
{
if (Pattern.matches(wildcardRegex, type.getId()))
{
set.add(type);
}
}
}
}
}
}
private void addToClassSetFromElements(IConfigurationElement element, String childrenName, String attributeName,
Set<Class<?>> set)
{
for (IConfigurationElement child : element.getChildren(childrenName))
{
try
{
Class<?> c = ExtensionPointHelper.getClassForProperty(child, attributeName);
if (c != null)
{
set.add(c);
}
}
catch (ClassNotFoundException e)
{
logger.error("Error processing intent filter return type", e); //$NON-NLS-1$
}
}
}
/**
* @return Priority of this filter.
*/
public int getPriority()
{
return priority;
}
/**
* Set the priority of this filter. If two filter's match an Intent exactly,
* the priority is used to determine which one to use.
*
* @param priority
* @return this
*/
public IntentFilter setPriority(int priority)
{
this.priority = priority;
return this;
}
/**
* @return Collection of actions, any of which may match an Intent's action.
*/
public Set<String> getActions()
{
return actions;
}
/**
* Add an action to this filter.
*
* @param action
* @return this
*/
public IntentFilter addAction(String action)
{
actions.add(action);
return this;
}
/**
* Remove an action from this filter.
*
* @param action
* @return this
*/
public IntentFilter removeAction(String action)
{
actions.remove(action);
return this;
}
/**
* @return Collection of categories associated with this filter. All
* categories in an Intent must exist in this filter in order for it
* to be matched.
*/
public Set<String> getCategories()
{
return categories;
}
/**
* Add a category to this filter.
*
* @param category
* @return this
*/
public IntentFilter addCategory(String category)
{
categories.add(category);
return this;
}
/**
* Remove a category from this filter.
*
* @param category
* @return this
*/
public IntentFilter removeCategory(String category)
{
categories.remove(category);
return this;
}
/**
* @return Collection of content types that this filter can match against.
*/
public Set<IContentType> getContentTypes()
{
return contentTypes;
}
/**
* Add a content type to this filter.
*
* @param dataType
* @return this
*/
public IntentFilter addContentType(IContentType contentType)
{
contentTypes.add(contentType);
return this;
}
/**
* Remove a content type from this filter.
*
* @param dataType
* @return this
*/
public IntentFilter removeContentType(IContentType contentType)
{
contentTypes.remove(contentType);
return this;
}
/**
* @return The {@link URIFilter}s used to filter an Intent's URI.
*/
public Set<URIFilter> getURIFilters()
{
return uriFilters;
}
/**
* Add a {@link URIFilter} to this filter.
*
* @param uriFilter
* @return this
*/
public IntentFilter addURIFilter(URIFilter uriFilter)
{
uriFilters.add(uriFilter);
return this;
}
/**
* Remove a {@link URIFilter} from this filter.
*
* @param uriFilter
* @return this
*/
public IntentFilter removeURIFilter(URIFilter uriFilter)
{
uriFilters.remove(uriFilter);
return this;
}
/**
* @return Does this filter's handler return a result to the intent
* callback?
*/
public boolean isReturnsResult()
{
return returnsResult;
}
/**
* Set whether this filter's handler results a result to the intent
* callback.
*
* @param returnsResult
* @return this
*/
public IntentFilter setReturnsResult(boolean returnsResult)
{
this.returnsResult = returnsResult;
return this;
}
/**
* @return The return types that this filter supports, if known
*/
public Set<Class<?>> getReturnTypes()
{
return returnTypes;
}
/**
* Add a return type to this filter.
*
* @param returnType
* @return this
*/
public IntentFilter addReturnType(Class<?> returnType)
{
returnTypes.add(returnType);
return this;
}
/**
* Remove a return type from this filter.
*
* @param returnType
* @return this
*/
public IntentFilter removeReturnType(Class<?> returnType)
{
returnTypes.remove(returnType);
return this;
}
/**
* @return Class associated with this filter, used to perform the actual
* Intent handling.
*/
public Class<? extends IIntentHandler> getHandler()
{
return handler;
}
/**
* Set the Intent handler class associated with this filter.
*
* @param handler
*/
public void setHandler(Class<? extends IIntentHandler> handler)
{
this.handler = handler;
}
/**
* @return The label to show to the user if multiple filters match an
* intent.
*/
public String getLabel()
{
return label;
}
/**
* Set the label to show to the user if multiple filters match an intent.
*
* @param label
*/
public void setLabel(String label)
{
this.label = label;
}
/**
* @return The description to show to the user if multiple filters match an
* intent.
*/
public String getDescription()
{
return description;
}
/**
* Set the description to show to the user if multiple filters match an
* intent.
*
* @param description
*/
public void setDescription(String description)
{
this.description = description;
}
/**
* @return The icon to show to the user if multiple filters match an intent.
*/
public URL getIcon()
{
return icon;
}
/**
* Set the icon to show to the user if multiple filters match an intent.
*
* @param icon
*/
public void setIcon(URL icon)
{
this.icon = icon;
}
/**
* @return Should this filter be shown in the list of filters prompt when
* multiple filters match an Intent?
*/
public boolean isPrompt()
{
return prompt;
}
/**
* Set whether this filter be shown in the list of filters prompt when
* multiple filters match an Intent.
*
* @param prompt
*/
public void setPrompt(boolean prompt)
{
this.prompt = prompt;
}
/**
* Check if this filter matches the given Intent.
*
* @param intent
* Intent to check
* @return True if this filter matches.
*/
public boolean matches(Intent intent)
{
if (intent == null)
{
return false;
}
//first check intent action
if (!actions.isEmpty() && !actions.contains(intent.getAction()))
{
return false;
}
//next check that this contains all intent categories
if (!categories.containsAll(intent.getCategories()))
{
return false;
}
//if a content type is defined by one but not the other, no chance of matching
IContentType contentType = intent.getContentType();
if ((contentType == null) != contentTypes.isEmpty())
{
return false;
}
//if there are content types defined, check if any match the content type of the intent
if (!contentTypes.isEmpty())
{
if (!anyContentTypesMatch(contentType))
{
return false;
}
}
/*
//check content type
IContentType contentType = intent.getContentType();
if (contentType != null)
{
//if a content type is defined, but intent doesn't handle any content types, no chance of matching
if (contentTypes.isEmpty())
{
return false;
}
//if there are content types defined, check if any match the content type of the intent
if (!anyContentTypesMatch(contentType))
{
return false;
}
}
*/
//if the intent has an expected or required return type
if (intent.getExpectedReturnType() != null || intent.getRequiredReturnType() != null)
{
//check that this filter returns a result
if (!returnsResult)
{
return false;
}
//if the intent requires a certain type, check that one matches
if (intent.getRequiredReturnType() != null)
{
if (!anyReturnTypesMatch(intent.getRequiredReturnType()))
{
return false;
}
}
//if the filter knows which types it returns, check that at least one matches
else if (!returnTypes.isEmpty()) //expectedReturnType is non-null
{
if (!anyReturnTypesMatch(intent.getExpectedReturnType()))
{
return false;
}
}
}
else
{
//Sometimes an intent will be raised that doesn't know if it will produce a result or not, so an expected/required
//return type is not set. These intents should still be able to be matched by filters that return results.
}
//if there are any schemes/authorities/paths defined, check if any match the URI of the intent (in that order)
if (!uriFilters.isEmpty())
{
if (intent.getURI() == null)
{
return false;
}
if (!anyURIFiltersMatch(intent.getURI()))
{
return false;
}
}
return true;
}
protected boolean anyContentTypesMatch(IContentType expectedContentType)
{
if (expectedContentType != null)
{
for (IContentType contentType : contentTypes)
{
if (expectedContentType.isKindOf(contentType))
{
return true;
}
}
}
return false;
}
protected boolean anyReturnTypesMatch(Class<?> expectedReturnType)
{
if (expectedReturnType != null)
{
for (Class<?> returnType : returnTypes)
{
if (expectedReturnType.isAssignableFrom(returnType))
{
return true;
}
}
}
return false;
}
protected boolean anyURIFiltersMatch(URI uri)
{
for (URIFilter uriFilter : uriFilters)
{
if (uriFilter.matches(uri))
{
return true;
}
}
return false;
}
private static boolean isEmpty(String s)
{
return s == null || s.isEmpty();
}
}