/*******************************************************************************
* 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.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.RegistryFactory;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.swt.widgets.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.gov.ga.earthsci.intent.resolver.ContentTypeResolverManager;
import au.gov.ga.earthsci.intent.util.ContextInjectionFactoryThreadSafe;
/**
* Injectable {@link Intent} manager, used for starting intents. Contains a
* collection of the registered intent filters, and their associated handler.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class IntentManager implements IIntentManager
{
private static final String INTENT_FILTERS_ID = "au.gov.ga.earthsci.intent.filters"; //$NON-NLS-1$
private static final Logger logger = LoggerFactory.getLogger(IntentManager.class);
private static IIntentManager instance = new IntentManager();
/**
* @return An instance of the intent manager
*/
public static IIntentManager getInstance()
{
return instance;
}
/**
* Set the singleton instance of the intent manager. Generally should not be
* called, but handy for inserting implementations for unit testing.
*
* @param instance
*/
public static void setInstance(IIntentManager instance)
{
IntentManager.instance = instance;
}
private final List<IntentFilter> filters = new ArrayList<IntentFilter>();
private final LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<Runnable>();
private ExecutorService executor;
private IntentManager()
{
IConfigurationElement[] config = RegistryFactory.getRegistry().getConfigurationElementsFor(INTENT_FILTERS_ID);
for (IConfigurationElement element : config)
{
try
{
boolean isFilter = "filter".equals(element.getName()); //$NON-NLS-1$
if (isFilter)
{
IntentFilter filter = new IntentFilter(element);
filters.add(filter);
}
}
catch (Exception e)
{
logger.error("Error processing intent filter", e); //$NON-NLS-1$
}
}
}
@Override
public void beginExecution()
{
synchronized (executorQueue)
{
if (executor == null)
{
executor = Executors.newFixedThreadPool(5, new ThreadFactory()
{
private int count = 0;
@Override
public Thread newThread(Runnable r)
{
Thread thread = new Thread(r);
thread.setName("Intent thread " + (++count)); //$NON-NLS-1$
return thread;
}
});
for (Runnable runnable : executorQueue)
{
executor.execute(runnable);
}
executorQueue.clear();
}
}
}
@Override
public void start(Intent intent, IIntentCallback callback, IEclipseContext context)
{
start(intent, null, true, callback, context);
}
@Override
public void start(final Intent intent, final IIntentFilterSelectionPolicy selectionPolicy,
final boolean showProgress, final IIntentCallback callback, final IEclipseContext context)
{
synchronized (executorQueue)
{
Runnable runnable = new Runnable()
{
@Override
public void run()
{
//TODO add progress monitor if show progress is true
try
{
IntentFilter filter = null;
Class<? extends IIntentHandler> handlerClass = intent.getHandler();
if (handlerClass == null)
{
//if intent has no content type, try to determine it
if (intent.getContentType() == null && intent.isDetermineContentType())
{
IContentType contentType = determineContentType(intent, showProgress, context);
intent.setContentType(contentType);
}
//search through all registered filters for those that can handle the intent
List<IntentFilter> filters = findFilters(intent);
if (selectionPolicy != null)
{
//remove any filters that the selection filter disallows
Iterator<IntentFilter> iterator = filters.iterator();
while (iterator.hasNext())
{
if (!selectionPolicy.allowed(intent, iterator.next()))
{
iterator.remove();
}
}
}
if (!callback.filters(filters, intent))
{
return;
}
if (filters.isEmpty())
{
throw new Exception("Could not find filter to handle intent: " + intent); //$NON-NLS-1$
}
//select the filter to use to handle the intent
filter = selectFilter(filters, intent, context);
if (filter == null)
{
callback.aborted(intent);
return;
}
handlerClass = filter.getHandler();
if (handlerClass == null)
{
throw new Exception("Selected intent filter has no handler registered"); //$NON-NLS-1$
}
}
//create the handler, and notify the callback
IEclipseContext activeLeaf = context.getActiveLeaf();
IEclipseContext child = activeLeaf.createChild();
IIntentHandler handler = ContextInjectionFactoryThreadSafe.make(handlerClass, child);
if (!callback.starting(filter, handler, intent))
{
return;
}
//handle the intent
handler.handle(intent, callback);
}
catch (Exception e)
{
callback.error(e, intent);
}
}
};
if (executor == null)
{
executorQueue.add(runnable);
}
else
{
executor.execute(runnable);
}
}
}
protected IContentType determineContentType(final Intent intent, boolean showProgress, IEclipseContext context)
throws IOException
{
URL url = null;
try
{
url = intent.getURL();
}
catch (MalformedURLException e)
{
}
if (url == null)
{
return null;
}
InputStream is = new LazyURLInputStream(url);
try
{
return ContentTypeResolverManager.resolveContentType(url, intent);
}
finally
{
try
{
is.close();
}
catch (IOException e)
{
}
}
}
protected IntentFilter selectFilter(final List<IntentFilter> filters, final Intent intent, IEclipseContext context)
{
if (filters == null || filters.isEmpty())
{
return null;
}
if (filters.size() == 1)
{
return filters.get(0);
}
//show a dialog allowing the user to select which intent filter to use
final IntentSelectionDialog.Factory dialogFactory = new IntentSelectionDialog.Factory();
ContextInjectionFactoryThreadSafe.inject(dialogFactory, context);
final AtomicInteger index = new AtomicInteger(0);
Display.getDefault().syncExec(new Runnable()
{
@Override
public void run()
{
final IntentSelectionDialog dialog = dialogFactory.create(intent, filters);
if (dialog.open() == Dialog.CANCEL)
{
index.set(-1);
return;
}
index.set(dialog.getSelectedIndex());
}
});
if (index.get() < 0)
{
return null;
}
return filters.get(index.get());
}
/**
* Find the intent filters that match the given intent. Returns an empty
* list if none could be found.
* <p/>
* The best match is defined as follows:
* <ul>
* <li>If the intent defines an expected return type, any filters that
* define that return type are preferred over those that don't</li>
* <li>If the intent defines a content type, the filters that define a
* content type closer to the intent's content type are preferred</li>
* <li>Otherwise the first matching filter is returned</li>
* </ul>
*
* @param intent
* Intent to find a filter for
* @return Intent filters that match the given intent
*/
protected List<IntentFilter> findFilters(Intent intent)
{
//TODO is matching expected return type more important than content type distance?
//right now, matched filter list is ordered by content type distance first
//add matching filters to a list, prioritising any that have a matching return type
List<IntentFilter> matches = new ArrayList<IntentFilter>();
int matchExpectedReturnTypeIndex = 0;
for (IntentFilter filter : filters)
{
if (filter.matches(intent))
{
if (filter.anyReturnTypesMatch(intent.getExpectedReturnType()))
{
matches.add(matchExpectedReturnTypeIndex++, filter);
}
else
{
matches.add(filter);
}
}
}
removeFiltersWithSuperclassHandlers(matches);
removeNonPromptFiltersIfPromptFilterExists(matches);
//if no matches or only 1, return list
if (matches.isEmpty() || matches.size() == 1)
{
return matches;
}
//if the content type is defined, find the distances to the filter's content type
Map<IntentFilter, Integer> contentTypeDistances = null;
IContentType contentType = intent.getContentType();
if (contentType != null)
{
contentTypeDistances = new HashMap<IntentFilter, Integer>();
for (IntentFilter filter : matches)
{
int distance = ContentTypeHelper.distanceToClosestMatching(contentType, filter.getContentTypes());
if (distance < 0)
{
//content type doesn't match, put at the end
distance = Integer.MAX_VALUE;
}
contentTypeDistances.put(filter, distance);
}
}
final Map<IntentFilter, Integer> contentTypeDistancesFinal = contentTypeDistances;
//sort matches by content type distance
//if distances are the same (or no distances were calculated), sort by priority
Collections.sort(matches, new Comparator<IntentFilter>()
{
@Override
public int compare(IntentFilter o1, IntentFilter o2)
{
if (contentTypeDistancesFinal != null)
{
Integer d1 = contentTypeDistancesFinal.get(o1);
Integer d2 = contentTypeDistancesFinal.get(o2);
int compare = d1.compareTo(d2);
if (compare != 0)
{
return compare;
}
}
return -((Integer) o1.getPriority()).compareTo(o2.getPriority());
}
});
return matches;
}
private void removeFiltersWithSuperclassHandlers(List<IntentFilter> filters)
{
Iterator<IntentFilter> iterator = filters.iterator();
while (iterator.hasNext())
{
IntentFilter filter = iterator.next();
if (hasFilterWithSubclassHandler(filters, filter))
{
iterator.remove();
}
}
}
private void removeNonPromptFiltersIfPromptFilterExists(List<IntentFilter> filters)
{
boolean anyPromptFilters = false, anyNonPromptFilters = false;
for (IntentFilter filter : filters)
{
anyPromptFilters |= filter.isPrompt();
anyNonPromptFilters |= !filter.isPrompt();
}
if (anyPromptFilters && anyNonPromptFilters)
{
Iterator<IntentFilter> iterator = filters.iterator();
while (iterator.hasNext())
{
IntentFilter filter = iterator.next();
if (!filter.isPrompt())
{
iterator.remove();
}
}
}
}
private boolean hasFilterWithSubclassHandler(List<IntentFilter> filters, IntentFilter filter)
{
Class<? extends IIntentHandler> handler = filter.getHandler();
if (handler != null)
{
for (IntentFilter f : filters)
{
if (f == filter)
{
//skip itself
continue;
}
Class<? extends IIntentHandler> otherHandler = f.getHandler();
if (otherHandler == null)
{
continue;
}
if (handler.isAssignableFrom(otherHandler))
{
//found a handler that is a subclass of the filter's handler in question
return true;
}
}
}
return false;
}
@Override
public void addFilter(IntentFilter filter)
{
filters.add(filter);
}
@Override
public void removeFilter(IntentFilter filter)
{
filters.remove(filter);
}
}