/*
* Copyright (C) 2013, VistaTEC or third-party contributors as indicated
* by the @author tags or express copyright attribution statements applied by
* the authors. All third-party contributions are distributed under license by
* VistaTEC.
*
* This file is part of Ocelot.
*
* Ocelot is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Ocelot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, write to:
*
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301
* USA
*
* Also, see the full LGPL text here: <http://www.gnu.org/copyleft/lesser.html>
*/
package com.vistatec.ocelot.plugins;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.vistatec.ocelot.config.ConfigService;
import com.vistatec.ocelot.config.ConfigTransferService;
import com.vistatec.ocelot.events.EnrichingStartedStoppedEvent;
import com.vistatec.ocelot.events.EnrichmentViewEvent;
import com.vistatec.ocelot.events.LQIAdditionEvent;
import com.vistatec.ocelot.events.LQIEditEvent;
import com.vistatec.ocelot.events.LQIRemoveEvent;
import com.vistatec.ocelot.events.SegmentEditEvent;
import com.vistatec.ocelot.events.SegmentTargetEnterEvent;
import com.vistatec.ocelot.events.SegmentTargetExitEvent;
import com.vistatec.ocelot.events.api.OcelotEventQueue;
import com.vistatec.ocelot.events.api.OcelotEventQueueListener;
import com.vistatec.ocelot.freme.gui.EnrichmentFrame;
import com.vistatec.ocelot.its.model.LanguageQualityIssue;
import com.vistatec.ocelot.its.model.Provenance;
import com.vistatec.ocelot.plugins.ReportPlugin.ReportException;
import com.vistatec.ocelot.segment.model.BaseSegmentVariant;
import com.vistatec.ocelot.segment.model.OcelotSegment;
import com.vistatec.ocelot.services.SegmentService;
/**
* Detect, install, and instantiate any available plugin classes.
*
* This is meant to be used by calling discover(), and then kept around to
* provide access to instances of all the discovered plugins. The plugins
* themselves are instantiated immediately and are treated like stateful
* singletons.
*/
public class PluginManager implements OcelotEventQueueListener {
private static Logger LOG = LoggerFactory.getLogger(PluginManager.class);
private List<String> itsPluginClassNames = new ArrayList<String>();
private List<String> segPluginClassNames = new ArrayList<String>();
private List<String> reportPluginClassNames = new ArrayList<String>();
private List<String> fremePluginClassNames = new ArrayList<String>();
private List<String> qualityPluginClassNames = new ArrayList<String>();
private HashMap<ITSPlugin, Boolean> itsPlugins;
private HashMap<SegmentPlugin, Boolean> segPlugins;
private HashMap<ReportPlugin, Boolean> reportPlugins;
private HashMap<FremePlugin, Boolean> fremePlugins;
private FremePluginManager fremeManager;
private ClassLoader classLoader;
private File pluginDir;
private final ConfigService cfgService;
private QualityPluginManager qualityPluginManager;
public PluginManager(ConfigService cfgService, File pluginDir,
OcelotEventQueue eventQueue) {
this.itsPlugins = new HashMap<ITSPlugin, Boolean>();
this.segPlugins = new HashMap<SegmentPlugin, Boolean>();
this.reportPlugins = new HashMap<ReportPlugin, Boolean>();
this.fremePlugins = new HashMap<FremePlugin, Boolean>();
this.fremeManager = new FremePluginManager(eventQueue);
this.cfgService = cfgService;
this.pluginDir = pluginDir;
qualityPluginManager = new QualityPluginManager();
}
public File getPluginDir() {
return this.pluginDir;
}
public void setPluginDir(File pluginDir) {
this.pluginDir = pluginDir;
}
public Set<Plugin> getPlugins() {
Set<Plugin> plugins = new HashSet<Plugin>();
Set<? extends Plugin> itsPlugins = getITSPlugins();
Set<? extends Plugin> segmentPlugins = getSegmentPlugins();
Set<? extends Plugin> reportPlugins = getReportPlugins();
Set<? extends Plugin> fremePlugins = getFremePlugins();
Set<? extends Plugin> qualityPlugins = getQualityPlugins();
plugins.addAll(itsPlugins);
plugins.addAll(segmentPlugins);
plugins.addAll(reportPlugins);
plugins.addAll(fremePlugins);
plugins.addAll(qualityPlugins);
return plugins;
}
/**
* Get a list of available ITS plugin instances.
*/
public Set<ITSPlugin> getITSPlugins() {
return this.itsPlugins.keySet();
}
public Set<SegmentPlugin> getSegmentPlugins() {
return this.segPlugins.keySet();
}
public Set<ReportPlugin> getReportPlugins() {
return this.reportPlugins.keySet();
}
public Set<FremePlugin> getFremePlugins() {
return this.fremePlugins.keySet();
}
public Set<QualityPlugin> getQualityPlugins() {
return qualityPluginManager.getPlugins().keySet();
}
/**
* Return if the plugin should receive data from the workbench.
*/
public boolean isEnabled(Plugin plugin) {
boolean enabled = false;
if (plugin instanceof ITSPlugin) {
ITSPlugin itsPlugin = (ITSPlugin) plugin;
enabled = itsPlugins.get(itsPlugin);
} else if (plugin instanceof SegmentPlugin) {
SegmentPlugin segPlugin = (SegmentPlugin) plugin;
enabled = segPlugins.get(segPlugin);
} else if (plugin instanceof ReportPlugin) {
ReportPlugin reportPlugin = (ReportPlugin) plugin;
enabled = reportPlugins.get(reportPlugin);
} else if (plugin instanceof FremePlugin) {
FremePlugin fremePlugin = (FremePlugin) plugin;
enabled = fremePlugins.get(fremePlugin);
} else if (plugin instanceof QualityPlugin) {
QualityPlugin qualityPlugin = (QualityPlugin) plugin;
enabled = qualityPluginManager.getPlugins().get(qualityPlugin);
}
return enabled;
}
public void setEnabled(Plugin plugin, boolean enabled)
throws ConfigTransferService.TransferException {
if (plugin instanceof ITSPlugin) {
ITSPlugin itsPlugin = (ITSPlugin) plugin;
itsPlugins.put(itsPlugin, enabled);
} else if (plugin instanceof SegmentPlugin) {
SegmentPlugin segPlugin = (SegmentPlugin) plugin;
segPlugins.put(segPlugin, enabled);
} else if (plugin instanceof ReportPlugin) {
ReportPlugin reportPlugin = (ReportPlugin) plugin;
reportPlugins.put(reportPlugin, enabled);
} else if (plugin instanceof FremePlugin) {
FremePlugin fremePlugin = (FremePlugin) plugin;
fremePlugins.put(fremePlugin, enabled);
if (fremeManager.getFremeMenu(fremePlugin) != null) {
fremeManager.setFremeMenuEnabled(enabled);
}
} else if (plugin instanceof QualityPlugin) {
QualityPlugin qualityPlugin = (QualityPlugin) plugin;
qualityPluginManager.enablePlugin(qualityPlugin, enabled);
}
cfgService.savePluginEnabled(plugin, enabled);
}
/**
* Return the set of all {@link ITSPlugin} that are currently enabled.
*
* @return set of enabled plugins
*/
public Set<ITSPlugin> getEnabledITSPlugins() {
Set<ITSPlugin> enabled = new HashSet<ITSPlugin>();
for (ITSPlugin plugin : getITSPlugins()) {
if (isEnabled(plugin)) {
enabled.add(plugin);
}
}
return enabled;
}
/**
* ITSPlugin handler for exporting LQI/Provenance metadata of segments.
*
* @param sourceLang
* @param targetLang
* @param segmentService
*/
public void exportData(String sourceLang, String targetLang,
SegmentService segmentService) {
for (int row = 0; row < segmentService.getNumSegments(); row++) {
OcelotSegment seg = segmentService.getSegment(row);
List<LanguageQualityIssue> lqi = seg.getLQI();
List<Provenance> prov = seg.getProvenance();
for (ITSPlugin plugin : getEnabledITSPlugins()) {
try {
plugin.sendLQIData(sourceLang, targetLang, seg, lqi);
plugin.sendProvData(sourceLang, targetLang, seg, prov);
} catch (Exception e) {
LOG.error("ITS Plugin '" + plugin.getPluginName()
+ "' threw an exception on ITS metadata export", e);
}
}
}
}
/**
* SegmentPlugin handler for beginning a target segment edit.
*
* @param event
*/
@Subscribe
public void notifySegmentTargetEnter(SegmentTargetEnterEvent event) {
OcelotSegment seg = event.getSegment();
for (SegmentPlugin segPlugin : segPlugins.keySet()) {
if (isEnabled(segPlugin)) {
try {
segPlugin.onSegmentTargetEnter(seg);
} catch (Exception e) {
LOG.error("Segment plugin '" + segPlugin.getPluginName()
+ "' threw an exception on segment target enter", e);
}
}
}
}
/**
* SegmentPlugin handler for finishing a target segment edit.
*
* @param event
*/
@Subscribe
public void notifySegmentTargetExit(SegmentTargetExitEvent event) {
OcelotSegment seg = event.getSegment();
for (SegmentPlugin segPlugin : segPlugins.keySet()) {
if (isEnabled(segPlugin)) {
try {
segPlugin.onSegmentTargetExit(seg);
} catch (Exception e) {
LOG.error("Segment plugin '" + segPlugin.getPluginName()
+ "' threw an exception on segment target exit", e);
}
}
}
}
@Subscribe
public void enrichmentViewRequest(EnrichmentViewEvent e) {
try {
EnrichmentFrame enrichFrame = new EnrichmentFrame(e.getVariant(),
null);
SwingUtilities.invokeLater(enrichFrame);
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void notifyOpenFile(String filename, List<OcelotSegment> segments) {
for (SegmentPlugin segPlugin : segPlugins.keySet()) {
if (isEnabled(segPlugin)) {
try {
segPlugin.onFileOpen(filename);
} catch (Exception e) {
LOG.error("Segment plugin '" + segPlugin.getPluginName()
+ "' threw an exception on file open", e);
}
}
}
if(isReportPluginEnabled()){
ReportPlugin reportPlugin = reportPlugins.keySet().iterator().next();
reportPlugin.onOpenFile(filename, segments);
}
qualityPluginManager.initOpenedFileSettings(segments);
}
public void notifySaveFile(String filename) {
for (SegmentPlugin segPlugin : segPlugins.keySet()) {
if (isEnabled(segPlugin)) {
try {
segPlugin.onFileSave(filename);
} catch (Exception e) {
LOG.error("Segment plugin '" + segPlugin.getPluginName()
+ "' threw an exception on file save", e);
}
}
}
}
/**
* Search the default directory for plugins. Equivalent to
* <code>discover(getPluginDir())</code>.
*
* @throws IOException
*/
public void discover() throws IOException {
discover(getPluginDir());
}
/**
* Search the provided directory for any JAR files containing valid plugin
* classes. Instantiate and configure any such classes.
*
* @param pluginDirectory
* @throws IOException
* if something goes wrong reading the directory
*/
public void discover(File pluginDirectory) throws IOException {
if (!pluginDirectory.isDirectory()) {
return;
}
File[] jarFiles = pluginDirectory.listFiles(new JarFilenameFilter());
installClassLoader(jarFiles);
for (File f : jarFiles) {
scanJar(f);
}
for (String s : itsPluginClassNames) {
try {
@SuppressWarnings("unchecked")
Class<? extends ITSPlugin> c = (Class<ITSPlugin>) Class
.forName(s, false, classLoader);
ITSPlugin plugin = c.newInstance();
itsPlugins.put(plugin, cfgService.wasPluginEnabled(plugin));
} catch (ClassNotFoundException e) {
// XXX Shouldn't happen?
System.out.println("Warning: " + e.getMessage());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (String s : segPluginClassNames) {
try {
@SuppressWarnings("unchecked")
Class<? extends SegmentPlugin> c = (Class<SegmentPlugin>) Class
.forName(s, false, classLoader);
SegmentPlugin plugin = c.newInstance();
segPlugins.put(plugin, cfgService.wasPluginEnabled(plugin));
} catch (ClassNotFoundException e) {
// XXX Shouldn't happen?
System.out.println("Warning: " + e.getMessage());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (String s : reportPluginClassNames) {
try {
@SuppressWarnings("unchecked")
Class<? extends ReportPlugin> c = (Class<ReportPlugin>) Class
.forName(s, false, classLoader);
ReportPlugin plugin = c.newInstance();
reportPlugins.put(plugin, cfgService.wasPluginEnabled(plugin));
} catch (ClassNotFoundException e) {
// XXX Shouldn't happen?
System.out.println("Warning: " + e.getMessage());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (String s : fremePluginClassNames) {
try {
@SuppressWarnings("unchecked")
Class<? extends FremePlugin> c = (Class<FremePlugin>) Class
.forName(s, false, classLoader);
Constructor<? extends FremePlugin> constructor = c
.getDeclaredConstructor(String.class);
FremePlugin plugin = constructor.newInstance(pluginDir
.getAbsolutePath());
fremePlugins.put(plugin, false);
setEnabled(plugin, cfgService.wasPluginEnabled(plugin));
} catch (ClassNotFoundException e) {
// XXX Shouldn't happen?
System.out.println("Warning: " + e.getMessage());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (String s : qualityPluginClassNames) {
try {
@SuppressWarnings("unchecked")
Class<? extends QualityPlugin> c = (Class<QualityPlugin>) Class
.forName(s, false, classLoader);
QualityPlugin plugin = c.newInstance();
qualityPluginManager.getPlugins().put(plugin,
cfgService.wasPluginEnabled(plugin));
} catch (ClassNotFoundException e) {
// XXX Shouldn't happen?
System.out.println("Warning: " + e.getMessage());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void installClassLoader(File[] jarFiles) throws IOException {
final List<URL> pluginJarURLs = new ArrayList<URL>();
for (File file : jarFiles) {
// Make sure that this is actually a real jar
if (!isValidJar(file)) {
continue;
}
// TODO - this may break when the path contains whitespace
URL url = file.toURI().toURL();
pluginJarURLs.add(url);
}
classLoader = AccessController
.doPrivileged(new PrivilegedAction<URLClassLoader>() {
public URLClassLoader run() {
return new URLClassLoader(pluginJarURLs
.toArray(new URL[pluginJarURLs.size()]), Thread
.currentThread().getContextClassLoader());
}
});
}
void scanJar(final File file) {
try {
Enumeration<JarEntry> e = new JarFile(file).entries();
while (e.hasMoreElements()) {
JarEntry entry = e.nextElement();
String name = entry.getName();
if (name.endsWith(".class")) {
name = convertFileNameToClass(name);
try {
Class<?> clazz = Class
.forName(name, false, classLoader);
// Skip non-instantiable classes
if (clazz.isInterface()
|| Modifier.isAbstract(clazz.getModifiers())) {
continue;
}
if (ITSPlugin.class.isAssignableFrom(clazz)) {
// It's a plugin! Just store the name for now
// since we will need to reinstantiate it later with
// the
// real classloader (I think)
if (itsPluginClassNames.contains(name)) {
// TODO: log this
System.out
.println("Warning: found multiple implementations of plugin class "
+ name);
} else {
itsPluginClassNames.add(name);
}
} else if (SegmentPlugin.class.isAssignableFrom(clazz)) {
// It's a plugin! Just store the name for now
// since we will need to reinstantiate it later with
// the
// real classloader (I think)
if (segPluginClassNames.contains(name)) {
// TODO: log this
System.out
.println("Warning: found multiple implementations of plugin class "
+ name);
} else {
segPluginClassNames.add(name);
}
} else
if (ReportPlugin.class.isAssignableFrom(clazz)) {
// It's a plugin! Just store the name for now
// since we will need to reinstantiate it later with
// the
// real classloader (I think)
if (reportPluginClassNames.contains(name)) {
// TODO: log this
System.out
.println("Warning: found multiple implementations of plugin class "
+ name);
} else {
reportPluginClassNames.add(name);
} }else if (FremePlugin.class.isAssignableFrom(clazz)) {
// It's a plugin! Just store the name for now
// since we will need to reinstantiate it later with
// the
// real classloader (I think)
if (fremePluginClassNames.contains(name)) {
// TODO: log this
System.out
.println("Warning: found multiple implementations of plugin class "
+ name);
} else {
fremePluginClassNames.add(name);
} }
else if (QualityPlugin.class.isAssignableFrom(clazz)) {
// It's a plugin! Just store the name for now
// since we will need to reinstantiate it later with
// the
// real classloader (I think)
if (qualityPluginClassNames.contains(name)) {
// TODO: log this
System.out
.println("Warning: found multiple implementations of plugin class "
+ name);
} else {
qualityPluginClassNames.add(name);
}
}
} catch (ClassNotFoundException ex) {
// XXX shouldn't happen?
System.out.println("Warning: " + ex.getMessage());
}
}
}
} catch (IOException e) {
// XXX Log this and continue
e.printStackTrace();
}
}
private boolean isValidJar(File f) throws IOException {
JarInputStream is = new JarInputStream(new FileInputStream(f));
boolean rv = (is.getNextEntry() != null);
is.close();
return rv;
}
// Convert file name to a java class name
private String convertFileNameToClass(String filename) {
String s = filename.substring(0, filename.length() - 6);
return s.replace('/', '.');
}
static class JarFilenameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String filename) {
if (filename == null || filename.equals("")) {
return false;
}
int i = filename.lastIndexOf('.');
if (i == -1) {
return false;
}
String s = filename.substring(i);
return s.equalsIgnoreCase(".jar");
}
}
@Subscribe
public void handleEnrichingStartedStoppedEvent(
EnrichingStartedStoppedEvent event) {
fremeManager
.setEnriching(event.getAction() == EnrichingStartedStoppedEvent.STARTED);
FremeMenu fremeMenu = (FremeMenu) fremeManager
.getFremeMenu(fremePlugins.keySet().iterator().next());
if (fremeMenu != null) {
((FremeMenu) fremeMenu)
.setEnrichMenuEnabled(event.getAction() == EnrichingStartedStoppedEvent.STOPPED);
}
fremeManager
.setContextMenuItemEnabled(event.getAction() == EnrichingStartedStoppedEvent.STOPPED);
}
public void enrichSegments(List<OcelotSegment> segments) {
fremeManager.setSegments(segments);
enrichSegments(FremePluginManager.OVERRIDE_ENRICHMENTS);
}
public void setSourceAndTargetLangs(String sourceLang, String targetLang) {
if (fremePlugins != null && !fremePlugins.isEmpty()) {
int dashIdx = sourceLang.indexOf("-");
if (dashIdx != -1) {
sourceLang = sourceLang.substring(0, dashIdx);
}
dashIdx = targetLang.indexOf("-");
if (dashIdx != -1) {
targetLang = targetLang.substring(0, dashIdx);
}
fremePlugins.keySet().iterator().next()
.setSourceAndTargetLanguages(sourceLang, targetLang);
}
}
private void enrichSegments(int action) {
if (fremePlugins != null && !fremePlugins.isEmpty()) {
Entry<FremePlugin, Boolean> fremeEntry = fremePlugins.entrySet()
.iterator().next();
if (fremeEntry.getValue()) {
fremeManager.enrich(fremeEntry.getKey(), action);
}
}
}
@Subscribe
public void segmentEdit(SegmentEditEvent e) {
if (e.getSegment().getTarget() instanceof BaseSegmentVariant) {
enrichVariant((BaseSegmentVariant) e.getSegment().getTarget(), e
.getSegment().getSegmentNumber(), true,
FremePluginManager.OVERRIDE_ENRICHMENTS);
}
}
public void enrichVariant(BaseSegmentVariant variant, int segmentNumber,
boolean target, int action) {
if (fremePlugins != null && !fremePlugins.isEmpty()) {
Entry<FremePlugin, Boolean> fremeEntry = fremePlugins.entrySet()
.iterator().next();
if (fremeEntry.getValue()) {
fremeManager.enrich(fremeEntry.getKey(), variant,
segmentNumber, target, action);
}
}
}
private JMenu getFremeMenu() {
JMenu fremeMenu = null;
if (fremePlugins != null && !fremePlugins.isEmpty()) {
FremePlugin fremePlugin = fremePlugins.keySet().iterator().next();
fremeMenu = fremeManager.getFremeMenu(fremePlugin);
fremeMenu.setEnabled(fremePlugins.get(fremePlugin));
}
return fremeMenu;
}
public List<JMenu> getPluginMenuList(final JFrame ocelotFrame) {
List<JMenu> menuList = new ArrayList<JMenu>();
if(isReportPluginEnabled()){
JMenu reportMenu = new JMenu("Reports");
JMenuItem generateMenuItem = new JMenuItem("Generate Reports");
generateMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try {
reportPlugins.keySet().iterator().next()
.generateReport(ocelotFrame);
} catch (ReportException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
reportMenu.add(generateMenuItem);
menuList.add(reportMenu);
}
if (isFremePluginEnabled()) {
menuList.add(getFremeMenu());
}
if (qualityPluginManager.isQualityPluginLoaded()) {
qualityPluginManager.setOcelotMainFrame(ocelotFrame);
menuList.add(qualityPluginManager.getQualityPluginMenu());
}
return menuList;
}
public boolean isReportPluginEnabled() {
return reportPlugins != null && !reportPlugins.isEmpty()
&& reportPlugins.entrySet().iterator().next().getValue();
}
public boolean isFremePluginEnabled() {
return fremePlugins != null && !fremePlugins.isEmpty()
&& fremePlugins.entrySet().iterator().next().getValue();
}
@Subscribe
public void handleLqiDeleted(LQIRemoveEvent event){
qualityPluginManager.removedQualityIssue(event.getLQI());
}
@Subscribe
public void handleLqiAdded(LQIAdditionEvent event) {
qualityPluginManager.addQualityIssue(event.getLQI());
}
@Subscribe
public void handleLqiEdited(LQIEditEvent event) {
qualityPluginManager.editedQualityIssue(event.getOldLQI(),
event.getLQI());
}
public List<JMenuItem> getSegmentContextMenuItems(
final OcelotSegment segment, final BaseSegmentVariant variant,
final boolean target) {
List<JMenuItem> items = new ArrayList<JMenuItem>();
if (fremePlugins != null && !fremePlugins.isEmpty()) {
FremePlugin fremePlugin = fremePlugins.keySet().iterator().next();
if (fremePlugins.get(fremePlugin)) {
items = fremeManager.getSegmentContextMenuItems(fremePlugin,
segment, variant, target);
}
}
return items;
}
}