Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/RunScriptDialog.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/RunScriptDialog.java	(revision 25019)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/RunScriptDialog.java	(revision 25071)
@@ -14,4 +14,7 @@
 import java.io.FileReader;
 import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -175,6 +178,6 @@
 			 */
 			String currentFile = cbScriptFile.getText();
-			Main.pref.put(PREF_KEY_FILE_HISTORY, currentFile.trim());
-			Main.pref.putCollection(PREF_KEY_LAST_FILE, cbScriptFile.getHistory());			
+			Main.pref.put(PREF_KEY_LAST_FILE, currentFile.trim());
+			Main.pref.putCollection(PREF_KEY_FILE_HISTORY, cbScriptFile.getHistory());			
 		}
 		super.setVisible(visible);
@@ -272,24 +275,7 @@
 		
 		protected ScriptEngine getScriptEngine(File file) {
-			ScriptEngineManager mgr = new ScriptEngineManager(getClass().getClassLoader());
-			Matcher matcher = Pattern.compile("\\.([^\\.]+)$").matcher(file.toString());
+			ScriptEngine engine = ScriptEngineProvider.getInstance().getEngineForFile(file);
+			if (engine != null) return engine;
 			
-			// can we derive a suitable script engine from the file name?			
-			MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
-			// FIXME: provide a resources file for script mime types; provide an editor
-			// for these mappings in the preferences
-			mimeTypesMap.addMimeTypes("application/x-groovy groovy");
-			String mt = mimeTypesMap.getContentType(file);
-			if (mt != null){
-				ScriptEngine engine = mgr.getEngineByMimeType(mt);
-				if (engine != null) return engine;	
-			}
-			
-			// no script engine at all? Abort.
-			List<ScriptEngineFactory> factories = mgr.getEngineFactories();
-			if (factories.isEmpty()) {
-				warnNoScriptingEnginesInstalled();
-				return null;
-			}
 			// let the user select a script engine
 			return ScriptEngineSelectionDialog.select(RunScriptDialog.this); 
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptEngineProvider.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptEngineProvider.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptEngineProvider.java	(revision 25071)
@@ -0,0 +1,230 @@
+package org.openstreetmap.josm.plugins.scripting;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.activation.MimetypesFileTypeMap;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineFactory;
+import javax.script.ScriptEngineManager;
+import javax.swing.AbstractListModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.scripting.preferences.PreferenceKeys;
+import org.openstreetmap.josm.plugins.scripting.preferences.ScriptEngineJarInfo;
+/**
+ * <p>Provides a list model for the list of available script engines.</p>
+ */
+public class ScriptEngineProvider extends AbstractListModel implements PreferenceKeys{
+	static private final Logger logger = Logger.getLogger(ScriptEngineProvider.class.getName());
+	
+	static private ScriptEngineProvider instance;
+	static public ScriptEngineProvider getInstance() {
+		if (instance == null) {
+			instance = new ScriptEngineProvider();
+		}
+		return instance;
+	}
+	
+	
+	private final List<ScriptEngineFactory> factories = new ArrayList<ScriptEngineFactory>();
+	private final List<File> scriptEngineJars = new ArrayList<File>();
+	private MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
+		
+	protected void loadMimeTypesMap() {
+		File f = new File(ScriptingPlugin.getInstance().getPluginDir(), "mime.types");
+		if (f.isFile() && f.canRead()){
+			try {
+				mimeTypesMap = new MimetypesFileTypeMap(new FileInputStream(f));
+				return;
+			} catch(IOException e) {
+				System.err.println(tr("Warning: failed to load mime types from file ''0''.", f));
+				e.printStackTrace();
+			}
+		}
+		
+		InputStream is = null;
+		try {
+			is = getClass().getResourceAsStream("/resources/mime.types.default");
+			if (is == null){
+				System.err.println(tr("Warning: failed to load default mime types from  resource ''0''.", "/resources/mime.types.default"));
+				return;
+			}
+			mimeTypesMap = new MimetypesFileTypeMap(is);
+		} finally {
+			if (is != null) {try {is.close();}catch(IOException e){}}
+		}
+	}
+	
+	protected void restoreScriptEngineUrlsFromPreferences() {
+		scriptEngineJars.clear();
+		Collection<String> jars = Main.pref.getCollection(PREF_KEY_SCRIPTING_ENGINE_JARS,null);
+		if (jars == null) return;
+		for (String jar: jars){
+			jar = jar.trim();
+			if (jar.isEmpty()) continue;			
+			ScriptEngineJarInfo info = new ScriptEngineJarInfo(jar);
+			if (!info.getStatusMessage().equals(ScriptEngineJarInfo.OK_MESSAGE)) continue;
+			scriptEngineJars.add(new File(jar));
+		}		
+	}
+	
+	protected ClassLoader buildClassLoader() {
+		ClassLoader cl = null;
+		URL [] urls = new URL[scriptEngineJars.size()];
+		for(int i=0; i < scriptEngineJars.size(); i++){
+			try {
+				urls[i] = scriptEngineJars.get(i).toURI().toURL();
+			} catch(MalformedURLException e){
+				// shouldn't happen because the entries in 'scriptEngineJars' 
+				// are existing, valid files. Ignore the exception.
+				e.printStackTrace();
+				continue;
+			}
+		}
+		if (urls.length > 0){
+			return new URLClassLoader(
+					urls,
+					getClass().getClassLoader()
+			);
+		} else {
+			return getClass().getClassLoader();
+		}
+	}
+		
+	protected void loadScriptEngineFactories() {
+		try {
+			ClassLoader cl = buildClassLoader();
+			factories.clear();
+			ScriptEngineManager manager = new ScriptEngineManager(cl);
+			factories.addAll(manager.getEngineFactories());
+		} catch(Throwable t){
+			t.printStackTrace();
+			return;
+		} 
+		
+		Collections.sort(factories,
+				new Comparator<ScriptEngineFactory>() {
+					@Override
+					public int compare(ScriptEngineFactory f1, ScriptEngineFactory f2) {
+						return f1.getEngineName().compareTo(f2.getEngineName());
+					}
+				}
+		);	
+	}
+	
+	private ScriptEngineProvider(){
+		restoreScriptEngineUrlsFromPreferences();
+		loadScriptEngineFactories();
+		loadMimeTypesMap();
+		fireContentsChanged(this, 0, scriptEngineJars.size());
+	}
+	
+	/**
+	 * <p>Replies the list of jar files from which script engines are loaded.</p>
+	 * 
+	 * @return the list of jar files
+	 */
+	public List<File> getScriptEngineJars() {
+		return new ArrayList<File>(scriptEngineJars);
+	}
+	
+	/**
+	 * <p>Replies a script engine by name or null, if no such script engine exists.</p>
+	 * 
+	 * @param name the name 
+	 * @return the script engine
+	 * @see ScriptEngineManager#getEngineByName(String)
+	 */
+	public ScriptEngine getEngineByName(String name) {
+		ScriptEngineManager mgr = new ScriptEngineManager(buildClassLoader());
+		return mgr.getEngineByName(name);
+	}
+	
+	/**
+	 * <p>Replies a suitable script engine for a mime type or null, if no such script engine exists.</p>
+	 * 
+	 * @param name the mime type 
+	 * @return the script engine
+	 * @see ScriptEngineManager#getEngineByMimeType(String)
+	 */
+	public ScriptEngine getEngineByMimeType(String mimeType) {
+		ScriptEngineManager mgr = new ScriptEngineManager(buildClassLoader());
+		return mgr.getEngineByMimeType(mimeType);
+	}
+	
+	/**
+	 * <p>Replies a suitable script engine for a script file, if no such script engine exists.</p>
+	 * 
+	 * <p>Derives a mime type from the file suffix and replies a script engine suitable for this
+	 * mime type.</p>
+	 * 
+	 * @param scriptFile the script file
+	 * @return the script engine
+	 */
+	public ScriptEngine getEngineForFile(File scriptFile) {
+		if (scriptFile == null) return null;
+		ScriptEngineManager mgr = new ScriptEngineManager(buildClassLoader());
+		return mgr.getEngineByMimeType(mimeTypesMap.getContentType(scriptFile));
+	}
+	
+	/**
+	 * <p>Sets the list of jar files which provide JSR 226 compatible script
+	 * engines.</p>
+	 * 
+	 * <p>null entries in the list are ignored. Entries which aren't 
+	 * {@link ScriptEngineJarInfo#getStatusMessage() valid} are ignored.</p>
+	 * 
+	 * @param jars the list of jar files. Can be null to set an empty list of jar files.
+	 */
+	public void setScriptEngineJars(List<File> jars){
+		this.scriptEngineJars.clear();
+		if (jars != null){
+			for (File jar: jars){
+				if (jar == null) continue;
+				ScriptEngineJarInfo info = new ScriptEngineJarInfo(jar.toString());
+				if (! info.getStatusMessage().equals(ScriptEngineJarInfo.OK_MESSAGE)) continue;
+				this.scriptEngineJars.add(jar);
+			}
+		}
+		loadScriptEngineFactories();
+		fireContentsChanged(this, 0, scriptEngineJars.size());
+	}
+			
+	/**
+	 * <p>Replies a script engine created by the i-th script engine factory.</p>
+	 * 
+	 * @param i the index
+	 * @return the engine
+	 */
+	public ScriptEngine getScriptEngine(int i){
+		ScriptEngine engine = factories.get(i).getScriptEngine();
+		return engine;
+	}
+
+	/* ------------------------------------------------------------------------------------ */
+	/* ListModel                                                                            */
+	/* ------------------------------------------------------------------------------------ */
+	@Override
+	public Object getElementAt(int i) {
+		return factories.get(i);
+	}
+
+	@Override
+	public int getSize() {
+		return factories.size();
+	}		
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptEngineSelectionDialog.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptEngineSelectionDialog.java	(revision 25019)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptEngineSelectionDialog.java	(revision 25071)
@@ -13,14 +13,9 @@
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineFactory;
-import javax.script.ScriptEngineManager;
 import javax.swing.AbstractAction;
-import javax.swing.AbstractListModel;
 import javax.swing.BorderFactory;
 import javax.swing.JButton;
@@ -45,4 +40,5 @@
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.WindowGeometry;
+import org.openstreetmap.josm.plugins.scripting.ui.ScriptEngineCellRenderer;
 
 /**
@@ -81,5 +77,5 @@
 	private JButton btnOK;
 	private ScriptEngine selectedEngine;
-	private ScriptEngineListModel model;
+	private ScriptEngineProvider model;
 	private OKAction actOK;
 	
@@ -137,5 +133,5 @@
 	protected JPanel buildScriptEngineListPanel() {
 		JPanel pnl = new JPanel(new BorderLayout());
-		lstEngines = new JList(model = new ScriptEngineListModel());
+		lstEngines = new JList(model = ScriptEngineProvider.getInstance());
 		lstEngines.setCellRenderer(new ScriptEngineCellRenderer());
 		pnl.add(lstEngines, BorderLayout.CENTER);
@@ -222,102 +218,3 @@
 		super.setVisible(visible);
 	}
-
-	/**
-	 * <p>Provides a list model for the list of available script engines.</p>
-	 */
-	private static class ScriptEngineListModel extends AbstractListModel {
-		
-		private List<ScriptEngineFactory> factories;
-		
-		public ScriptEngineListModel(){
-			ScriptEngineManager mgr = new ScriptEngineManager(getClass().getClassLoader());
-			factories = new ArrayList<ScriptEngineFactory>(mgr.getEngineFactories());
-			Collections.sort(factories,
-					new Comparator<ScriptEngineFactory>() {
-						@Override
-						public int compare(ScriptEngineFactory f1, ScriptEngineFactory f2) {
-							return f1.getEngineName().compareTo(f2.getEngineName());
-						}
-					}
-			);	
-		}
-				
-		/**
-		 * <p>Replies a script engine created by the i-th script engine factory.</p>
-		 * 
-		 * @param i the index
-		 * @return the engine
-		 */
-		public ScriptEngine getScriptEngine(int i){
-			ScriptEngine engine = factories.get(i).getScriptEngine();
-			return engine;
-		}
-
-		@Override
-		public Object getElementAt(int i) {
-			return factories.get(i);
-		}
-
-		@Override
-		public int getSize() {
-			return factories.size();
-		}		
-	}
-	
-	/**
-	 * <p>Implements a list cell renderer for the list of scripting engines.</p>
-	 *
-	 */
-	private static class ScriptEngineCellRenderer implements ListCellRenderer {
-
-		private final JLabel lbl = new JLabel();
-		
-		protected String getDisplayName(ScriptEngineFactory factory){
-			return tr("{1} (with engine {0})", factory.getEngineName(), factory.getLanguageName());
-		}
-		
-		protected String getTooltipText(ScriptEngineFactory factory){
-			StringBuilder sb = new StringBuilder();
-			sb.append("<html>");
-			sb.append("<strong>").append(tr("Name:")).append("</strong> ").append(factory.getEngineName()).append("<br>");
-			sb.append("<strong>").append(tr("Version:")).append("</strong> ").append(factory.getEngineVersion()).append("<br>");
-			sb.append("<strong>").append(tr("Language:")).append("</strong> ").append(factory.getLanguageName()).append("<br>");
-			sb.append("<strong>").append(tr("Language version:")).append("</strong> ").append(factory.getLanguageVersion()).append("<br>");
-			sb.append("<strong>").append(tr("MIME-Types:")).append("</strong> ");
-			List<String> types = factory.getMimeTypes();
-			for(int i=0; i<types.size(); i++){
-				if (i > 0 )sb.append(", ");
-				sb.append(types.get(i));
-			}
-			sb.append("<br>");
-			sb.append("</html>");
-			
-			return sb.toString();
-		}
-		
-		protected void renderColors(boolean selected){
-			if (!selected){
-				lbl.setForeground(UIManager.getColor("List.foreground"));
-				lbl.setBackground(UIManager.getColor("List.background"));
-			} else {
-				lbl.setForeground(UIManager.getColor("List.selectionForeground"));
-				lbl.setBackground(UIManager.getColor("List.selectionBackground"));
-			}
-		}
-		
-		public ScriptEngineCellRenderer() {		
-			lbl.setOpaque(true);
-			lbl.setBorder(BorderFactory.createEmptyBorder(1, 3, 1, 3));
-			lbl.setIcon(ImageProvider.get("script-engine"));
-		}
-		
-		@Override
-		public Component getListCellRendererComponent(JList list, Object obj,int index, boolean isSelected, boolean cellHasFocus) {
-			ScriptEngineFactory factory = (ScriptEngineFactory)obj;
-			renderColors(isSelected);
-			lbl.setText(getDisplayName(factory));
-			lbl.setToolTipText(getTooltipText(factory));
-			return lbl;
-		}		
-	}
 }
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptingPlugin.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptingPlugin.java	(revision 25019)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ScriptingPlugin.java	(revision 25071)
@@ -1,11 +1,18 @@
 package org.openstreetmap.josm.plugins.scripting;
 
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.awt.event.KeyEvent;
+
 import javax.swing.JMenu;
+import javax.swing.JSeparator;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
 import org.openstreetmap.josm.plugins.Plugin;
 import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.scripting.preferences.ConfigureAction;
+import org.openstreetmap.josm.plugins.scripting.preferences.PreferenceEditor;
 
 public class ScriptingPlugin extends Plugin {
@@ -14,6 +21,5 @@
 	
 	public static ScriptingPlugin getInstance() {
-		return instance;
-	
+		return instance;	
 	}
 	
@@ -26,7 +32,14 @@
 	protected void installScriptsMenu(){
 		JMenu mnuMacro;
-		Main.main.menu.add(mnuMacro = new JMenu(tr("Scripts")));
+		mnuMacro = Main.main.menu.addMenu(tr("Scripting"), KeyEvent.VK_S, Main.main.menu.defaultMenuPos, ht("/Plugin/Scripting"));
 		mnuMacro.setMnemonic('S');
-		mnuMacro.add(new RunScriptAction());		
+		mnuMacro.add(new RunScriptAction());
+		mnuMacro.add(new JSeparator());
+		mnuMacro.add(new ConfigureAction());
+	}
+
+	@Override
+	public PreferenceSetting getPreferenceSetting() {
+		return new PreferenceEditor();
 	}
 }
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ConfigureAction.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ConfigureAction.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ConfigureAction.java	(revision 25071)
@@ -0,0 +1,81 @@
+package org.openstreetmap.josm.plugins.scripting.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.event.ActionEvent;
+import java.lang.reflect.Field;
+
+import javax.swing.JTabbedPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.preferences.PreferenceDialog;
+import org.openstreetmap.josm.plugins.scripting.RunScriptDialog;
+
+public class ConfigureAction extends JosmAction {
+	public ConfigureAction() {
+		super(
+			tr("Configure..."),        // title
+			"preference", 			    // icon name
+			tr("Configure scripting preferences"),  // tooltip 
+			null,                // no shortcut 
+			false                // don't register
+		);		
+	}
+	
+	protected Component getChildByName(Component parent, String name){
+		if (parent == null) return null;
+		if (name == null) return null;
+		if (name.equals(parent.getName())) return parent;
+		if (parent instanceof Container) {
+			for (Component child: ((Container)parent).getComponents()) {
+				Component found = getChildByName(child, name);
+				if (found != null) return found;
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public void actionPerformed(ActionEvent evt) {
+		PreferenceDialog dialog = new PreferenceDialog(Main.parent);
+		
+		/*
+		 * FIXME: JOSM core doesn't provide a method to jump to a specific preference
+		 * tab in the preference dialog. We use reflection as work around.
+		 * Fix this as soon as the JOSM core provides a better way.
+		 * 
+		 * Exception handling: just dump the stack to the console, no further action
+		 * required. If we can't open the preference editor at the tab for 
+		 * scripting preferences, it is started at the default start position
+		 * instead. Not optimal, but no big deal.
+		 */
+		try {
+			Field f = PreferenceDialog.class.getDeclaredField("tpPreferences");
+			f.setAccessible(true);
+			JTabbedPane tp = (JTabbedPane)f.get(dialog);
+			/*
+			 * lookup the preferences tab which is an ancestor of our scripting
+			 * preferences panel
+			 */
+			for(int i=0; i< tp.getTabCount(); i++) {
+				if (getChildByName(tp.getComponentAt(i), "scripting.preferences.editor") != null) {
+					tp.setSelectedIndex(i);
+					break;
+				}
+			}
+		} catch(NoSuchFieldException e) {
+			e.printStackTrace();
+		} catch(SecurityException e){
+			e.printStackTrace();
+		} catch (IllegalArgumentException e) {
+			e.printStackTrace();
+		} catch (IllegalAccessException e) {
+			e.printStackTrace();
+		} finally {
+			dialog.setVisible(true);
+		}
+	}		
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/PreferenceEditor.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/PreferenceEditor.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/PreferenceEditor.java	(revision 25071)
@@ -0,0 +1,44 @@
+package org.openstreetmap.josm.plugins.scripting.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.tools.GBC;
+
+public class PreferenceEditor extends JPanel implements PreferenceSetting {
+	
+	private JTabbedPane tpPreferenceTabs;
+	private ScriptEnginesConfigurationPanel pnlScriptEngineConfiguration;
+	
+	public PreferenceEditor(){
+		build();
+	}
+	
+	protected void build() {
+		setLayout(new BorderLayout());
+		
+		tpPreferenceTabs = new JTabbedPane();
+		tpPreferenceTabs.add(tr("Script engines"), pnlScriptEngineConfiguration = new ScriptEnginesConfigurationPanel());
+		add(tpPreferenceTabs, BorderLayout.CENTER);
+	}
+
+	@Override
+	public void addGui(PreferenceTabbedPane gui) {
+        String description = tr("Configure script engines and scripts");
+        JPanel tab = gui.createPreferenceTab("script-engine", tr("Scripting"), description);        
+        tab.add(this, GBC.eol().fill(GBC.BOTH));
+        this.setName("scripting.preferences.editor");
+	}
+
+	@Override
+	public boolean ok() {
+		pnlScriptEngineConfiguration.persistToPreferences();
+		return false;
+	}
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/PreferenceKeys.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/PreferenceKeys.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/PreferenceKeys.java	(revision 25071)
@@ -0,0 +1,12 @@
+package org.openstreetmap.josm.plugins.scripting.preferences;
+
+public interface PreferenceKeys {
+	/**
+	 * <p>Preference entry for a list of jar files (full path per file) providing
+	 * JSR 223 compatible scripting engines.</p>
+	 * 
+	 * <p><strong>Default:</strong></p> - empty collection
+	 */
+	String PREF_KEY_SCRIPTING_ENGINE_JARS = "scripting.engine-jars";
+
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEngineJarInfo.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEngineJarInfo.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEngineJarInfo.java	(revision 25071)
@@ -0,0 +1,143 @@
+package org.openstreetmap.josm.plugins.scripting.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+/**
+ * <p>Represents a jar file which potentially provides a JSR 223 compatible scripting
+ * engine.</p>
+ */
+public class ScriptEngineJarInfo implements Comparable<ScriptEngineJarInfo>{
+	/**
+	 * Replied by {@link #getStatusMessage()}, if the jar file actually exists, is
+	 * readable, and provides a JSR 223 compatible scripting engine.
+	 */
+	static public final String OK_MESSAGE = "OK";
+	
+	private String jarFileName;
+	private String statusMessage = null;
+	
+	/**
+	 * <p>Analyses whether the jar file is providing a JSR 223 compatible scripting engine.
+	 * Invoke {@link #getStatusMessage()} to retrieve the respective status message.</p>
+	 */
+	public void analyse(){
+		File jar = null;
+		jar = new File(jarFileName);		
+		if (!jar.exists()) {
+			statusMessage = tr("''{0}'' doesn''t exist.", jar);
+			return;
+		}
+		if (! jar.isFile()) {
+			System.out.println("jar="+jar);
+			statusMessage = tr("''{0}'' is a directory. Expecting a jar file instead.", jar);
+			return;
+		}
+		if (! jar.canRead()) {
+			statusMessage = tr("''{0}'' isn't readable. Can''t load a script engine from this file.", jar);
+			return;
+		}
+		JarFile jf = null;
+		try {
+			jf = new JarFile(jar);
+		} catch (IOException e) {
+			statusMessage = tr("Failed to open file ''{0}'' as jar file. Can''t load a script engine from this file", jar);
+			return;
+		}
+		ZipEntry ze = jf.getEntry("META-INF/services/javax.script.ScriptEngineFactory");
+		if (ze == null){
+			statusMessage = tr("The jar file ''{0}'' doesn''t provide a script engine. The entry ''{1}'' is missing.", jar,"/META-INF/services/javax.script.ScriptEngineFactory");
+			return;
+		}
+		statusMessage = OK_MESSAGE;
+	}
+	
+	/**
+	 * <p>Creates a new info object for a script engine jar.</p>
+	 * 
+	 * @param jar the jar file. Empty string assumed, if null.
+	 */
+	public ScriptEngineJarInfo(String fileName) {
+		if (fileName == null) fileName = "";
+		this.jarFileName = fileName.trim();
+		analyse();
+	}
+		
+	/**
+	 * <p>Replies a localized status message describing the error status of this
+	 * scripting jar file or {@link #OK_MESSAGE} if this jar file is OK.</p>
+	 *   
+	 * @return the status message 
+	 * @throws IOException 
+	 */
+	public String getStatusMessage() {
+		if (statusMessage == null) analyse();
+		return statusMessage;		
+	}
+	
+	/**
+	 * <p>Replies the full path of the jar file.</p>
+	 * @return the path
+	 */
+	public String getJarFilePath() {
+		return jarFileName;
+	}
+	
+	/**
+	 * <p>Sets the path of the jar file.</p>
+	 * 
+	 * @param path the path. Assumes "" if null.
+	 */
+	public void setJarFilePath(String path){
+		if (path == null) path = "";
+		path = path.trim();
+		this.jarFileName = path;
+		analyse();
+	}
+	
+	public String toString() {
+		return MessageFormat.format("<scriptJarInfo for=''{0}'' />", jarFileName);
+	}
+
+	/* ----------------------------------------------------------------------------- */
+	/* interface Comparable                                                          */                    
+	/* ----------------------------------------------------------------------------- */
+	@Override
+	public int compareTo(ScriptEngineJarInfo o) {
+		if (o == null) return -1;
+		return jarFileName.compareTo(o.jarFileName);
+	}
+
+	/* ----------------------------------------------------------------------------- */
+	/* hashCode and equals                                                           */                    
+	/* ----------------------------------------------------------------------------- */	
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result
+				+ ((jarFileName == null) ? 0 : jarFileName.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ScriptEngineJarInfo other = (ScriptEngineJarInfo) obj;
+		if (jarFileName == null) {
+			if (other.jarFileName != null)
+				return false;
+		} else if (!jarFileName.equals(other.jarFileName))
+			return false;
+		return true;
+	}
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEngineJarTableModel.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEngineJarTableModel.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEngineJarTableModel.java	(revision 25071)
@@ -0,0 +1,151 @@
+package org.openstreetmap.josm.plugins.scripting.preferences;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.ListSelectionModel;
+import javax.swing.table.AbstractTableModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.scripting.ScriptEngineProvider;
+
+/**
+ * <p><strong>ScriptEngineJarTableModel</strong> is a table model for the table of script
+ * engine jars in the preferences dialog.</p>
+ *
+ */
+public class ScriptEngineJarTableModel extends AbstractTableModel implements PreferenceKeys{
+	static private final Logger logger = Logger.getLogger(ScriptEngineJarTableModel.class.getName());
+	
+	private final List<ScriptEngineJarInfo> jars = new ArrayList<ScriptEngineJarInfo>();
+	private DefaultListSelectionModel selectionModel;
+	
+	public ScriptEngineJarTableModel() {
+		this(null);
+	}
+	
+	/**
+	 * Creates the model
+	 * 
+	 * @param selectionModel the selection model to be used. Internally creates a selection model,
+	 * if null
+	 */
+	public ScriptEngineJarTableModel(DefaultListSelectionModel selectionModel) {
+		this.selectionModel = selectionModel;
+		if (this.selectionModel == null) this.selectionModel = new DefaultListSelectionModel();
+	}
+	
+	/**
+	 * <p>Replies the selection model known to this table model.</p>
+	 * 
+	 * <p>Make sure that a table using an instance of this model, is also using the 
+	 * respective selection model.</p>
+	 * <pre>
+	 *    model = new ScriptEngineJarTableModel();
+	 *    JTable table = new JTable(model);
+	 *    table.setSelectionModel(model.getSelectionModel()); 
+	 * </pre>
+	 * @return
+	 */
+	public ListSelectionModel getSelectionModel() {
+		return selectionModel;
+	}
+
+	@Override
+	public int getColumnCount() {
+		return 2;
+	}
+
+	@Override
+	public int getRowCount() {
+		return jars.size();
+	}
+
+	@Override
+	public Object getValueAt(int row, int col) {
+		return jars.get(row);
+	}
+
+	@Override
+	public void setValueAt(Object value, int row, int col) {
+		if (col == 1){
+			jars.get(row).setJarFilePath((String)value);
+			fireTableDataChanged();
+		}
+	}
+
+	@Override
+	public boolean isCellEditable(int row, int col) {
+		return col == 1; 
+	}
+	
+	/**
+	 * <p>Restores the configured jar files from preferences.</p>
+	 */
+	public void restoreFromPreferences() {
+		jars.clear();
+		Collection<String> paths = Main.pref.getCollection(PREF_KEY_SCRIPTING_ENGINE_JARS);
+		if (paths != null) {
+			for (String path: paths){
+				path = path.trim();
+				if (path.isEmpty()) continue;
+				jars.add(new ScriptEngineJarInfo(path));
+			}
+		}
+		fireTableDataChanged();
+	}
+	
+	
+	/**
+	 * <p>Persists the jar paths to the preferences.</p>
+	 */
+	public void persistToPreferences() {
+		Collection<String> paths = new ArrayList<String>();
+		for (ScriptEngineJarInfo info: jars) {
+			String path = info.getJarFilePath();
+			path = path.trim();
+			if (path.isEmpty()) continue;
+			paths.add(path);
+		}
+		Main.pref.putCollection(PREF_KEY_SCRIPTING_ENGINE_JARS, paths);
+	}
+	
+	public void deleteSelected(){
+		boolean updated = false;
+		for (int i = jars.size() -1; i >= 0; i--){
+			if (selectionModel.isSelectedIndex(i)) {
+				updated = true;
+				jars.remove(i);
+			}
+		}
+		if (updated){
+			fireTableDataChanged();
+		}		
+	}
+	
+	public void addNew(){
+		jars.add(new ScriptEngineJarInfo(""));
+		fireTableDataChanged();
+	}
+
+	@Override
+	public void fireTableDataChanged() {
+		super.fireTableDataChanged();
+		
+		// propagate the new list of scripte engine jars to the global script engine
+		// provider. 
+		List<File> jarfiles = new ArrayList<File>();
+		for (ScriptEngineJarInfo info: jars) {
+			String path = info.getJarFilePath();
+			path = path.trim();
+			if (path.isEmpty()) continue;
+			if (! info.getStatusMessage().equals(ScriptEngineJarInfo.OK_MESSAGE)) continue;
+			jarfiles.add(new File(path));
+		}
+		ScriptEngineProvider.getInstance().setScriptEngineJars(jarfiles);
+	}
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEnginesConfigurationPanel.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEnginesConfigurationPanel.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/preferences/ScriptEnginesConfigurationPanel.java	(revision 25071)
@@ -0,0 +1,423 @@
+package org.openstreetmap.josm.plugins.scripting.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.util.EventObject;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.event.CellEditorListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+
+import org.openstreetmap.josm.gui.util.TableCellEditorSupport;
+import org.openstreetmap.josm.gui.widgets.HtmlPanel;
+import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
+import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
+import org.openstreetmap.josm.plugins.scripting.ScriptEngineProvider;
+import org.openstreetmap.josm.plugins.scripting.ui.ScriptEngineCellRenderer;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * <p><strong>ScriptEnginesConfigurationPanel</strong> allows to configure 
+ * the script engines available in JOSM.</p>
+ * 
+ */
+public class ScriptEnginesConfigurationPanel extends VerticallyScrollablePanel{
+	
+	private ScriptEngineJarTableModel model;
+	private JTable tblJarFiles;
+	private RemoveJarAction actDelete;
+	
+	public ScriptEnginesConfigurationPanel() {
+		build();
+		model.restoreFromPreferences();
+	}
+	
+	protected JPanel buildScriptEnginesInfoPanel() {
+		JPanel pnl = new JPanel(new BorderLayout());
+		JList lstEngines = new JList(ScriptEngineProvider.getInstance());
+		lstEngines.setCellRenderer(new ScriptEngineCellRenderer());
+		lstEngines.setVisibleRowCount(3);
+		pnl.add(lstEngines, BorderLayout.CENTER);
+		lstEngines.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+		return pnl;
+	}
+	
+	protected JPanel buildScriptEngineJarsPanel() {
+		JPanel pnl = new JPanel(new GridBagLayout());
+		pnl.setBorder(
+				BorderFactory.createCompoundBorder(
+						BorderFactory.createEmptyBorder(3,3,3,3),
+						BorderFactory.createTitledBorder(tr("JAR files"))
+				)
+		);
+
+		GridBagConstraints gc = new GridBagConstraints();
+		
+		HtmlPanel info = new HtmlPanel();
+		info.setText(
+				"<html>"
+				+ tr("Enter additional JAR files which provide script engines.")
+				+ "</html>"
+		);
+		gc.gridx = 0; gc.gridy = 0;
+		gc.weightx = 1.0; gc.weighty = 0.0;
+		gc.fill = GridBagConstraints.BOTH;
+		pnl.add(info, gc);
+		
+		model = new ScriptEngineJarTableModel();
+		tblJarFiles= new JTable(model, new ColumnModel());
+		tblJarFiles.setSelectionModel(model.getSelectionModel());
+		tblJarFiles.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+		tblJarFiles.setRowHeight(new JButton("...").getPreferredSize().height);
+		
+		JScrollPane jp = new JScrollPane(tblJarFiles);
+		jp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+		jp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+		jp.setMinimumSize(new Dimension(0, 100));
+		
+		gc.gridx = 0; gc.gridy = 1;
+		gc.weightx = 1.0; gc.weighty = 1.0;
+		gc.fill = GridBagConstraints.BOTH;
+		pnl.add(jp, gc);
+	
+		JPanel ctrlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+		AddJarAction actAdd = new AddJarAction();
+		ctrlPanel.add(new JButton(actAdd));
+		actDelete = new RemoveJarAction();
+		ctrlPanel.add(new JButton(actDelete));
+		model.getSelectionModel().addListSelectionListener(actDelete);
+		tblJarFiles.getActionMap().put("deleteSelection", actDelete);
+		tblJarFiles.getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),"deleteSelection");
+		tblJarFiles.getActionMap().put("insertRow", actAdd);
+		tblJarFiles.getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0),"insertRow");
+		gc.gridx = 0; gc.gridy = 2;
+		gc.weightx = 1.0; gc.weighty = 0.0;
+		gc.fill = GridBagConstraints.HORIZONTAL;
+		pnl.add(ctrlPanel, gc);
+		return pnl;
+	}
+	
+	protected JPanel buildScriptEnginesPanel() {
+		JPanel pnl = new JPanel(new GridBagLayout());	
+		pnl.setBorder(
+				BorderFactory.createCompoundBorder(
+						BorderFactory.createEmptyBorder(3,3,3,3),
+						BorderFactory.createTitledBorder(tr("Available script engines"))
+				)
+		);
+		GridBagConstraints gc = new GridBagConstraints();
+		
+		HtmlPanel info = new HtmlPanel();
+		info.setText(
+				"<html>"
+				+ tr("JOSM currently supports the following script engines:")
+				+ "</html>"
+		);
+		gc.gridx = 0; gc.gridy = 0;
+		gc.weightx = 1.0; gc.weighty = 0.0;
+		gc.fill = GridBagConstraints.HORIZONTAL;
+		gc.insets = new Insets(3,3,3,3);
+		pnl.add(info, gc);
+
+		gc.gridx = 0; gc.gridy = 1;
+		gc.weightx = 1.0; gc.weighty = 1.0;
+		gc.fill = GridBagConstraints.BOTH;
+		gc.insets = new Insets(3,3,3,3);
+		pnl.add(buildScriptEnginesInfoPanel(), gc);
+		return pnl;
+	}
+	
+	protected void build() {
+		setLayout(new GridBagLayout());
+		GridBagConstraints gc = new GridBagConstraints();
+		gc.gridx = 0; gc.gridy = 0;
+		gc.weightx = 1.0; gc.weighty = 0.0;
+		gc.fill = GridBagConstraints.HORIZONTAL;
+		add(buildScriptEnginesPanel(), gc);
+		
+		gc.gridx = 0; gc.gridy = 1;
+		gc.weightx = 1.0; gc.weighty = 0.0;
+		gc.fill = GridBagConstraints.HORIZONTAL;
+		add(buildScriptEngineJarsPanel(), gc);
+
+		// filler 
+		gc.gridx = 0; gc.gridy = 2;
+		gc.weightx = 1.0; gc.weighty = 1.0;
+		gc.fill = GridBagConstraints.BOTH;
+		add(new JPanel(), gc);
+	}
+	
+	public void persistToPreferences() {
+		model.persistToPreferences();
+	}
+
+	public void restoreFromPreferences() {
+		model.restoreFromPreferences();
+	}
+	
+	static private class ColumnModel extends DefaultTableColumnModel {		
+		public ColumnModel() {
+			TableColumn tc;
+			ScriptEngineJarCellRenderer renderer = new ScriptEngineJarCellRenderer();
+			
+			tc = new TableColumn(0);
+			tc.setHeaderValue("");
+			tc.setMaxWidth(30);
+			tc.setPreferredWidth(30);
+			tc.setMinWidth(30);
+			tc.setResizable(false);
+			tc.setCellRenderer(renderer);			
+			addColumn(tc);
+			
+			tc = new TableColumn(1);
+			tc.setHeaderValue(tr("JAR file"));
+			tc.setCellRenderer(renderer);
+			tc.setCellEditor(new JarFileNameEditor());
+			tc.setResizable(true);			
+			addColumn(tc);
+		}
+	}
+	
+	static private class ScriptEngineJarCellRenderer extends JLabel implements TableCellRenderer {
+
+		public ScriptEngineJarCellRenderer(){
+			setOpaque(true);
+		}
+		
+		protected void reset() {
+			setIcon(null);
+			setText("");
+			setForeground(UIManager.getColor("Table.foreground"));
+			setBackground(UIManager.getColor("Table.background"));
+			setHorizontalAlignment(SwingConstants.LEFT);
+		}
+		
+		protected void renderColors(boolean selected){
+			if (!selected){
+				setForeground(UIManager.getColor("Table.foreground"));
+				setBackground(UIManager.getColor("Table.background"));
+			} else {
+				setForeground(UIManager.getColor("Table.selectionForeground"));
+				setBackground(UIManager.getColor("Table.selectionBackground"));
+			}
+		}
+		
+		protected void renderJarName(ScriptEngineJarInfo jar) {
+			String fileName = jar.getJarFilePath();
+			File f = new File(fileName.trim());
+			File parent = f.getParentFile();
+			if (parent != null){
+				String parentName= parent.getName();
+				if (parentName.length() > 15){
+					setText(parentName.substring(0, 10) + "..."  + File.pathSeparator + f.getName());										
+				} else {
+					setText(f.toString());
+				}
+			} else {
+				setText(f.toString());
+			}
+		}
+		
+		protected void renderJarStatus(ScriptEngineJarInfo jar){
+			setHorizontalAlignment(SwingConstants.CENTER);
+			String msg = jar.getStatusMessage();
+			if (jar.getJarFilePath().trim().isEmpty()) {
+				setIcon(null);
+				setToolTipText("");
+			} else if (msg.equals(ScriptEngineJarInfo.OK_MESSAGE)){
+				setIcon(ImageProvider.get("valid"));
+				setToolTipText("");
+			} else {
+				setIcon(ImageProvider.get("error"));
+				setToolTipText(msg);
+			}
+		}
+		
+		@Override
+		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+			ScriptEngineJarInfo jar = (ScriptEngineJarInfo)value;
+			reset();
+			switch(column){
+			case 0: renderJarStatus(jar); break;
+			case 1:
+				renderColors(isSelected);
+				renderJarName(jar); 
+				break;
+			}
+			return this;
+		}		
+	}
+	
+	private class RemoveJarAction extends AbstractAction implements ListSelectionListener{
+
+		public RemoveJarAction() {
+			putValue(NAME, tr("Remove"));
+			putValue(SHORT_DESCRIPTION, tr("Remove the selected jar files"));
+			putValue(SMALL_ICON, ImageProvider.get("dialogs","delete"));
+			updateEnabledState();
+		}		
+		
+		@Override
+		public void actionPerformed(ActionEvent arg0) {
+			model.deleteSelected();			
+		}
+		
+		public void updateEnabledState() {
+			setEnabled(!model.getSelectionModel().isSelectionEmpty());
+		}
+
+		@Override
+		public void valueChanged(ListSelectionEvent e) {
+			updateEnabledState();			
+		}
+	}
+	
+	private class AddJarAction extends AbstractAction {
+
+		public AddJarAction() {
+			putValue(NAME, tr("Add"));
+			putValue(SHORT_DESCRIPTION, tr("Add a jar file providing a script engine"));
+			putValue(SMALL_ICON, ImageProvider.get("add"));
+		}		
+		
+		@Override
+		public void actionPerformed(ActionEvent arg0) {
+			model.addNew();		
+			tblJarFiles.editCellAt(tblJarFiles.getRowCount()-1, 1);
+		}
+	}
+	
+	private static class JarFileNameEditor extends JPanel implements TableCellEditor {
+		static private final Logger logger = Logger.getLogger(JarFileNameEditor.class.getName());
+
+		private JTextField tfJarFile;
+		private JButton btnLauchFileChooser;
+		private TableCellEditorSupport tableCellEditorSupport;
+		private ScriptEngineJarInfo info;
+		
+		public JarFileNameEditor() {
+			tableCellEditorSupport = new TableCellEditorSupport(this);
+			build();
+		}
+		
+		protected void build() {
+			setLayout(new GridBagLayout());
+			GridBagConstraints gc = new GridBagConstraints();
+			gc.gridx = 0; gc.gridy = 0;
+			gc.weightx = 1.0; gc.weighty = 1.0;
+			gc.fill = GridBagConstraints.BOTH;
+			add(tfJarFile = new JTextField(), gc);
+			SelectAllOnFocusGainedDecorator.decorate(tfJarFile);
+			
+			gc.gridx = 1; gc.gridy = 0;
+			gc.weightx = 0.0; gc.weighty = 1.0;
+			gc.fill = GridBagConstraints.VERTICAL;
+			add(btnLauchFileChooser = new JButton(new LaunchFileChooserAction()), gc);
+			
+		}
+
+		public void addCellEditorListener(CellEditorListener l) {
+			tableCellEditorSupport.addCellEditorListener(l);
+		}
+
+		public void cancelCellEditing() {
+			tfJarFile.setText(info.getJarFilePath());
+			tableCellEditorSupport.fireEditingCanceled();
+		}
+
+		public Object getCellEditorValue() {
+			return tfJarFile.getText();
+		}
+
+		public boolean isCellEditable(EventObject anEvent) {
+			if (anEvent instanceof MouseEvent) {
+				return ((MouseEvent)anEvent).getClickCount() == 2;
+			}
+			return false;
+		}
+
+		public void removeCellEditorListener(CellEditorListener l) {
+			tableCellEditorSupport.removeCellEditorListener(l);
+		}
+
+		public boolean shouldSelectCell(EventObject anEvent) {
+			return true;
+		}
+
+		public boolean stopCellEditing() {
+			tableCellEditorSupport.fireEditingStopped();
+			return true;
+		}
+
+		@Override
+		public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+			info = (ScriptEngineJarInfo)value;
+			tfJarFile.setText(info.getJarFilePath());
+			tfJarFile.selectAll();
+			return this;
+		}
+		
+		private class LaunchFileChooserAction extends AbstractAction {
+			public LaunchFileChooserAction() {
+				putValue(NAME, "...");
+				putValue(SHORT_DESCRIPTION, tr("Launch file chooser"));				
+			}
+			
+			@Override
+			public void actionPerformed(ActionEvent arg0) {
+				String fileName = tfJarFile.getText().trim();
+				File currentFile = null;
+				if (! fileName.isEmpty()) {
+					currentFile = new File(fileName);
+				}
+				JFileChooser chooser = new JFileChooser();
+				chooser.setDialogTitle(tr("Select a jar file"));
+				chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+				chooser.setMultiSelectionEnabled(false);
+				if (currentFile != null){
+					chooser.setCurrentDirectory(currentFile);
+					chooser.setSelectedFile(currentFile);
+				}
+				int ret = chooser.showOpenDialog(btnLauchFileChooser);			
+				if (ret == JFileChooser.APPROVE_OPTION) {				
+					currentFile = chooser.getSelectedFile();
+					tfJarFile.setText(currentFile.toString());		
+					stopCellEditing();
+				} else {
+					tfJarFile.requestFocusInWindow();					
+				}
+			}
+		}
+		
+	}
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ui/ScriptEngineCellRenderer.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ui/ScriptEngineCellRenderer.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/ui/ScriptEngineCellRenderer.java	(revision 25071)
@@ -0,0 +1,72 @@
+package org.openstreetmap.josm.plugins.scripting.ui;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.util.List;
+
+import javax.script.ScriptEngineFactory;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import javax.swing.UIManager;
+
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * <p>Implements a list cell renderer for the list of scripting engines.</p>
+ *
+ */
+public class ScriptEngineCellRenderer implements ListCellRenderer {
+
+	private final JLabel lbl = new JLabel();
+	
+	protected String getDisplayName(ScriptEngineFactory factory){
+		return tr("{1} (with engine {0})", factory.getEngineName(), factory.getLanguageName());
+	}
+	
+	protected String getTooltipText(ScriptEngineFactory factory){
+		StringBuilder sb = new StringBuilder();
+		sb.append("<html>");
+		sb.append("<strong>").append(tr("Name:")).append("</strong> ").append(factory.getEngineName()).append("<br>");
+		sb.append("<strong>").append(tr("Version:")).append("</strong> ").append(factory.getEngineVersion()).append("<br>");
+		sb.append("<strong>").append(tr("Language:")).append("</strong> ").append(factory.getLanguageName()).append("<br>");
+		sb.append("<strong>").append(tr("Language version:")).append("</strong> ").append(factory.getLanguageVersion()).append("<br>");
+		sb.append("<strong>").append(tr("MIME-Types:")).append("</strong> ");
+		List<String> types = factory.getMimeTypes();
+		for(int i=0; i<types.size(); i++){
+			if (i > 0 )sb.append(", ");
+			sb.append(types.get(i));
+		}
+		sb.append("<br>");
+		sb.append("</html>");
+		
+		return sb.toString();
+	}
+	
+	protected void renderColors(boolean selected){
+		if (!selected){
+			lbl.setForeground(UIManager.getColor("List.foreground"));
+			lbl.setBackground(UIManager.getColor("List.background"));
+		} else {
+			lbl.setForeground(UIManager.getColor("List.selectionForeground"));
+			lbl.setBackground(UIManager.getColor("List.selectionBackground"));
+		}
+	}
+	
+	public ScriptEngineCellRenderer() {		
+		lbl.setOpaque(true);
+		lbl.setBorder(BorderFactory.createEmptyBorder(1, 3, 1, 3));
+		lbl.setIcon(ImageProvider.get("script-engine"));
+	}
+	
+	@Override
+	public Component getListCellRendererComponent(JList list, Object obj,int index, boolean isSelected, boolean cellHasFocus) {
+		ScriptEngineFactory factory = (ScriptEngineFactory)obj;
+		renderColors(isSelected);
+		lbl.setText(getDisplayName(factory));
+		lbl.setToolTipText(getTooltipText(factory));
+		return lbl;
+	}		
+}
Index: applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/util/Assert.java
===================================================================
--- applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/util/Assert.java	(revision 25071)
+++ applications/editors/josm/plugins/scripting/src/org/openstreetmap/josm/plugins/scripting/util/Assert.java	(revision 25071)
@@ -0,0 +1,22 @@
+package org.openstreetmap.josm.plugins.scripting.util;
+
+import java.text.MessageFormat;
+
+public class Assert {
+
+	public static void assertArgNotNull(Object arg, String name){
+		if (arg == null){
+			throw new IllegalArgumentException(
+					MessageFormat.format("parameter ''{0}'' must not be null", name)
+			);
+		}
+	}
+	
+	public static void assertArg(boolean condition, String message, Object...objs) {
+		if (!condition){
+			throw new IllegalArgumentException(
+					MessageFormat.format(message, objs)
+			);
+		}
+	}
+}
