/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.scripting;
import java.io.*;
import java.net.MalformedURLException;
import java.util.*;
import org.w3c.dom.Document;
import util.*;
import util.xml.*;
import com.leafdigital.ui.api.*;
import leafchat.core.api.*;
/** Scripting tool/window. */
@UIHandler("scripting")
public class ScriptingTool implements SimpleTool
{
private PluginContext context;
private Window w;
/** UI: Dependencies table */
public Table dependenciesUI;
/** UI: Plugins table */
public Table pluginsUI;
/** UI: Scripts table */
public Table scriptsUI;
/** UI: Create plugin button */
public Button createUI;
/** UI: Edit script button */
public Button editScriptUI;
/** UI: Delete script button */
public Button deleteScriptUI;
/** UI: Export script button */
public Button exportScriptUI;
/** UI: Shortname edit */
public EditBox shortNameUI;
/** UI: Display name edit */
public EditBox displayNameUI;
/** UI: Classname edit */
public EditBox classNameUI;
/** UI: Author edit */
public EditBox authorUI;
/** UI: Domain edit */
public EditBox domainUI;
/** UI: Target folder edit */
public EditBox targetUI;
/** UI: Show system plugins checkbox */
public CheckBox showSystemUI;
/** UI: Plugin name */
public Label pluginNameUI;
/** UI: Plugin description */
public Label pluginDescriptionUI;
/** UI: Plugin authors */
public Label pluginAuthorsUI;
/** UI: Plugin location */
public Label pluginLocationUI;
/** UI: Main choice panel */
public ChoicePanel scriptMainUI;
private boolean changedClassName;
private HashMap<Script, ScriptEditor> editors = new HashMap<Script, ScriptEditor>();
/**
* @param context Plugin context
*/
public ScriptingTool(PluginContext context)
{
this.context=context;
}
@Override
public int getDefaultPosition()
{
return 500;
}
@Override
public void removed()
{
}
@Override
public void clicked() throws GeneralException
{
if(w==null)
{
UI u=context.getSingle(UI.class);
w=u.createWindow("scripting", this);
w.setRemember("tool", "scripting");
initWindow();
w.show(false);
}
else
{
w.activate();
}
}
/** Callback: Script window closed. */
@UIAction
public void windowClosed()
{
w=null;
changedClassName=false;
}
private static class ExportedPackage implements Comparable<ExportedPackage>
{
String name;
int maxVersion;
ExportedPackage(File jar, String name, int maxVersion)
{
this.name=name;
this.maxVersion=maxVersion;
}
@Override
public int compareTo(ExportedPackage o)
{
return name.compareTo(o.name);
}
}
private final static int COL_PACKAGE=0,COL_VERSION=1,COL_REQUIRED=2;
private void initWindow()
{
targetUI.setFlag(EditBox.FLAG_ERROR);
PluginInfo[] plugins=context.getSingle(PluginList.class).getPluginList();
SortedSet<ExportedPackage> sortedPackages = new TreeSet<ExportedPackage>();
for(int i=0;i<plugins.length;i++)
{
PluginExport[] exports=plugins[i].getPluginExports();
for(int j=0;j<exports.length;j++)
{
sortedPackages.add(new ExportedPackage(plugins[i].getJar(),exports[j].getPackage(),exports[j].getMaxVersion()));
}
}
for(ExportedPackage ep : sortedPackages)
{
int index=dependenciesUI.addItem();
dependenciesUI.setString(index,COL_PACKAGE,ep.name);
dependenciesUI.setString(index,COL_VERSION,""+ep.maxVersion);
if(ep.name.matches("com.leafdigital.(irc|ui).api"))
dependenciesUI.setBoolean(index,COL_REQUIRED,true);
}
changeShowSystem();
selectPlugins();
updateScripts();
}
@Override
public String getLabel()
{
return "Scripting";
}
@Override
public String getThemeType()
{
return "scriptingButton";
}
/** Callback: Browse button. */
@UIAction
public void actionBrowse()
{
String existing=targetUI.getValue();
if(existing.length()==0)
existing=PlatformUtils.getDocumentsFolder();
File selected=context.getSingle(UI.class).showFolderSelect(
null,"Select parent folder", new File(existing));
if(selected!=null)
{
targetUI.setValue(selected.getAbsolutePath());
changeTarget();
}
}
/**
* User clicks Help button.
*/
@UIAction
public void actionHelp()
{
try
{
PlatformUtils.showBrowser((new File("help/scripts/index.html")).toURI().toURL());
}
catch(MalformedURLException e)
{
ErrorMsg.report("Failed to open help",e);
}
catch(IOException e)
{
ErrorMsg.report(e.getMessage(),e.getCause());
}
}
/**
* Callback: Plugin create button.
* @throws GeneralException
*/
@UIAction
public void actionCreate() throws GeneralException
{
try
{
File target=new File(targetUI.getValue(),shortNameUI.getValue());
if(target.exists())
{
context.getSingle(UI.class).showUserError(w,
"Folder exists", "The project folder " + target
+ " already exists. If you want to overwrite it, you must manually delete it first.");
return;
}
target.mkdir();
// Make the API jar file
File lib=new File(target,"lib");
lib.mkdir();
List<String> packagesList = new LinkedList<String>();
for(int index=0; index<dependenciesUI.getNumItems(); index++)
{
if(dependenciesUI.getBoolean(index,COL_REQUIRED))
packagesList.add(dependenciesUI.getString(index,COL_PACKAGE));
}
String[] packages=packagesList.toArray(new String[packagesList.size()]);
context.getSingle(PluginList.class).saveAPIJar(
packages,
new File(lib,"leafchat.selectedapi.jar"));
// Flip the package and make the relevant source folder
String domainName=domainUI.getValue();
String packageName="";
while(domainName.length()>0)
{
packageName+=domainName.replaceAll("^.*\\.","")+".";
domainName=domainName.replaceAll("(^[^.]+$)|(\\.[^.]+$)","");
}
packageName+=shortNameUI.getValue();
File src=new File(target,"src/"+packageName.replace('.','/'));
src.mkdirs();
// Make the plugininfo.xml
String pluginInfo=IOUtils.loadString(getClass().getResourceAsStream("template/plugininfo.xml.template"));
pluginInfo=pluginInfo.replaceAll("%%DISPLAYNAME%%",XML.esc(displayNameUI.getValue()));
pluginInfo=pluginInfo.replaceAll("%%AUTHOR%%",XML.esc(authorUI.getValue()));
pluginInfo=pluginInfo.replaceAll("%%CLASSNAME%%",packageName+"."+classNameUI.getValue()+"Plugin");
String dependencies="";
for(int index=0;index<dependenciesUI.getNumItems();index++)
{
if(dependenciesUI.getBoolean(index,COL_REQUIRED))
{
dependencies+=
" <api>\n <package>"+
dependenciesUI.getString(index,COL_PACKAGE)+
"</package>\n <version>"+
dependenciesUI.getString(index,COL_VERSION)+
"</version>\n </api>\n";
}
}
pluginInfo=pluginInfo.replaceAll("%%DEPENDENCIES%%",dependencies);
FileOutputStream fos=new FileOutputStream(new File(src,"plugininfo.xml"));
fos.write(pluginInfo.getBytes("UTF-8"));
fos.close();
// Make the sample class file
String java=IOUtils.loadString(getClass().getResourceAsStream("template/Plugin.java.template"));
java=java.replaceAll("%%DISPLAYNAME%%",displayNameUI.getValue().replaceAll("([\\\"])","\\$1"));
java=java.replaceAll("%%CLASSNAME%%",classNameUI.getValue()+"Plugin");
java=java.replaceAll("%%PACKAGENAME%%",packageName);
fos=new FileOutputStream(new File(src,classNameUI.getValue()+"Plugin.java"));
fos.write(java.getBytes("UTF-8"));
fos.close();
// Make the ant script
String ant=IOUtils.loadString(getClass().getResourceAsStream("template/build.xml.template"));
ant=ant.replaceAll("%%DISPLAYNAME%%",XML.esc(displayNameUI.getValue()));
ant=ant.replaceAll("%%SHORTNAME%%",shortNameUI.getValue());
ant=ant.replaceAll("%%USERPLUGINS%%",XML.esc(PlatformUtils.getUserFolder()+"/plugins"));
fos=new FileOutputStream(new File(target,"build.xml"));
fos.write(ant.getBytes("UTF-8"));
fos.close();
createUI.setEnabled(false);
if(!PlatformUtils.systemOpen(target))
{
context.getSingle(UI.class).showUserError(w,
"Unable to open folder", "Because you are using an older Java version "
+ "or a platform that doesn't support opening folders, leafChat "
+ "cannot show the folder for you. However, the plugin template has "
+ "been created at <key>" + target.getAbsolutePath() + "</key>.");
return;
}
return;
}
catch(IOException e)
{
throw new GeneralException("Error saving project",e);
}
}
/** Callback: Plugin target changed. */
@UIAction
public void changeTarget()
{
File f=new File(targetUI.getValue());
targetUI.setFlag((f.isDirectory() && f.canWrite()) ? EditBox.FLAG_NORMAL : EditBox.FLAG_ERROR);
changeEdit();
}
/** Callback: Class name changed. */
@UIAction
public void changeClassName()
{
changedClassName = true;
changeEdit();
}
/** Callback: Short name changed. */
@UIAction
public void changeShortName()
{
if(!changedClassName && shortNameUI.getFlag() == EditBox.FLAG_NORMAL)
{
classNameUI.setValue(
shortNameUI.getValue().substring(0,1).toUpperCase()
+ shortNameUI.getValue().substring(1));
}
changeEdit();
}
/** Callback: Some edit changed. */
@UIAction
public void changeEdit()
{
createUI.setEnabled(
shortNameUI.getFlag()==EditBox.FLAG_NORMAL &&
displayNameUI.getFlag()==EditBox.FLAG_NORMAL &&
classNameUI.getFlag()==EditBox.FLAG_NORMAL &&
authorUI.getFlag()==EditBox.FLAG_NORMAL &&
domainUI.getFlag()==EditBox.FLAG_NORMAL &&
targetUI.getFlag()==EditBox.FLAG_NORMAL
);
}
// Plugins view tab
///////////////////
private final static int PCOL_PLUGIN=0,PCOL_VERSION=1;
PluginInfo[] pluginsList;
/** Callback: System plugin checkbox changed. */
@UIAction
public void changeShowSystem()
{
boolean evenSystem=showSystemUI.isChecked();
PluginInfo[] plugins=
context.getSingle(PluginList.class).getPluginList();
List<PluginInfo> includedPlugins = new LinkedList<PluginInfo>();
for(int i=0;i<plugins.length;i++)
{
if(plugins[i].isSystem() && !evenSystem) continue;
if(plugins[i].isUserScript()) continue;
includedPlugins.add(plugins[i]);
}
pluginsList=includedPlugins.toArray(new PluginInfo[includedPlugins.size()]);
pluginsUI.clear();
for(int i=0;i<pluginsList.length;i++)
{
int index=pluginsUI.addItem();
pluginsUI.setString(index,PCOL_PLUGIN,pluginsList[i].getName());
pluginsUI.setString(index,PCOL_VERSION,pluginsList[i].getVersion());
}
}
/**
* Callback: Plugin selected.
*/
@UIAction
public void selectPlugins()
{
int index=pluginsUI.getSelectedIndex();
if(index==Table.NONE)
{
pluginNameUI.setText("(Select plugin for information)");
pluginDescriptionUI.setText("");
pluginAuthorsUI.setText("");
pluginLocationUI.setText("");
}
else
{
PluginInfo current=pluginsList[index];
pluginNameUI.setText(XML.esc(current.getName())+" <s>"+current.getVersion()+"</s>");
pluginDescriptionUI.setText(XML.esc(current.getDescription()));
String authors="";
for(int i=0;i<current.getAuthors().length;i++)
{
authors+="<line>"+XML.esc(current.getAuthors()[i])+"</line>";
}
pluginAuthorsUI.setText(authors);
try
{
pluginLocationUI.setText(XML.esc(current.getJar().getCanonicalPath()));
}
catch(IOException e)
{
throw new BugException(e);
}
}
}
private Script[] displayedScripts;
private final static int SCOL_NAME=0,SCOL_ERRORS=1,SCOL_ENABLED=2;
private void updateScripts()
{
int indexBefore=scriptsUI.getSelectedIndex();
Script selectedBefore=indexBefore==Table.NONE ? null : displayedScripts[indexBefore];
scriptsUI.clear();
displayedScripts=((ScriptingPlugin)context.getPlugin()).getScripts();
for(int i=0;i<displayedScripts.length;i++)
{
scriptsUI.addItem();
Script s=displayedScripts[i];
scriptsUI.setString(i,SCOL_NAME,s.getName());
updateScriptDetails(i,s);
if(s==selectedBefore)
scriptsUI.setSelectedIndex(i);
}
selectScripts();
scriptMainUI.display(displayedScripts.length==0 ? "noneChoice" : "tableChoice");
}
private void updateScriptDetails(int i,Script s)
{
scriptsUI.setString(i,SCOL_ERRORS,s.hasErrors() ? s.getErrorCount()+" error"+(s.getErrorCount()==1 ? "" : "s") : "");
scriptsUI.setBoolean(i,SCOL_ENABLED,s.isEnabled());
scriptsUI.setEditable(i,SCOL_ENABLED,!s.isChanged() && s.hasJar() && !s.hasErrors());
scriptsUI.setDim(i,SCOL_NAME,s.isChanged());
}
/** Callback: Script selection change. */
@UIAction
public void selectScripts()
{
int index=scriptsUI.getSelectedIndex();
boolean gotOne=index!=Table.NONE;
editScriptUI.setEnabled(gotOne);
// You can only delete scripts that don't have unsaved changes
deleteScriptUI.setEnabled(gotOne && !displayedScripts[index].isChanged());
exportScriptUI.setEnabled(gotOne && !displayedScripts[index].isChanged());
}
/**
* Callback: Script enable tickbox changed.
* @param index Table row
* @param column Table column
* @param before Previous value
* @throws GeneralException
*/
@UIAction
public void changeScripts(int index,int column,Object before) throws GeneralException
{
boolean enabled=scriptsUI.getBoolean(index,column);
displayedScripts[index].setEnabled(enabled);
}
/** Dialog for handling new scripts. */
@UIHandler("newscript")
public class NewScript
{
/** UI: Script name */
public EditBox nameUI;
/** UI: Create button */
public Button createUI;
private Dialog d;
NewScript() throws GeneralException
{
d = context.getSingle(UI.class).createDialog("newscript", this);
d.show(w);
}
/** Callback: Name changed. */
@UIAction
public void changeName()
{
if(ScriptingPlugin.isNewNameOkay(nameUI.getValue()))
{
nameUI.setFlag(EditBox.FLAG_NORMAL);
createUI.setEnabled(true);
}
else
{
nameUI.setFlag(EditBox.FLAG_ERROR);
createUI.setEnabled(false);
}
}
/**
* Callback: Create button.
* @throws GeneralException
*/
@UIAction
public void actionCreate() throws GeneralException
{
d.close();
((ScriptingPlugin)context.getPlugin()).newScript(nameUI.getValue(),
new Runnable()
{
@Override
public void run()
{
updateScripts();
scriptsUI.setSelectedIndex(displayedScripts.length-1);
informChanged(displayedScripts[displayedScripts.length-1]);
}
});
}
/**
* Callback: Cancel button.
*/
@UIAction
public void actionCancel()
{
d.close();
}
}
/**
* Callback: New script button.
* @throws GeneralException
*/
@UIAction
public void actionNewScript() throws GeneralException
{
new NewScript();
}
/**
* Callback: Import script button.
* @throws GeneralException
*/
@UIAction
public void actionImportScript() throws GeneralException
{
UI ui=context.getSingle(UI.class);
File selected=ui.showFileSelect(w,"Choose script to import",false,
new File(PlatformUtils.getDesktopFolder()),null,new String[] {".leafChatScript"},
"leafChat scripts");
if(selected==null) return;
if(!selected.getName().matches(Script.ALLOWED_NAMES))
{
ui.showUserError(w,"Error importing script","Script filenames must end in .leafChatScript and must not contain any special characters before that; only A-Z, a-z, 0-9 and space are permitted.");
return;
}
File f;
try
{
// Disable script before importing it
Document d=XML.parse(selected);
if(!d.getDocumentElement().getTagName().equals("script") ||
!d.getDocumentElement().hasAttribute("enabled"))
throw new XMLException("Not valid");
if(XML.getIntAttribute(d.getDocumentElement(),"version")!=1)
{
ui.showUserError(w,"Error importing script","This version of the script format is not supported. Check that you have the latest leafChat version.");
return;
}
d.getDocumentElement().setAttribute("enabled","n");
f=new File(ScriptingPlugin.scriptsFolder,selected.getName());
if(f.exists())
{
ui.showUserError(w,"Error importing script","You already have a script of the same name.");
return;
}
XML.save(f,d);
}
catch(XMLException e)
{
ui.showUserError(w,"Error importing script","Not a valid leafChat script.");
return;
}
final Script s;
try
{
s=new Script(context,f);
}
catch(GeneralException e)
{
f.delete();
ErrorMsg.report("Error importing script",e);
return;
}
((ScriptingPlugin)context.getPlugin()).newScript(s,new Runnable()
{
@Override
public void run()
{
updateScripts();
scriptsUI.setSelectedIndex(displayedScripts.length-1);
informChanged(displayedScripts[displayedScripts.length-1]);
UI ui=context.getSingle(UI.class);
ui.showQuestion(w,"Import successful",
"<para><strong>"+s.getName()+"</strong> was successfully imported. This " +
"script has been disabled. To enable it, click the checkbox by its name.</para>" +
"<para>It is <strong>critically important</strong> that you do not enable " +
"any scripts unless they were written by you or someone you trust. Scripts " +
"can easily <strong>damage your computer</strong>. Installing an unknown " +
"leafChat script is just as dangerous as running an unknown application " +
"program, so please take care.</para>",
UI.BUTTON_YES,"OK",null,null,UI.BUTTON_YES);
}
});
}
/**
* Callback: Export script button.
* @throws GeneralException
*/
@UIAction
public void actionExportScript() throws GeneralException
{
UI ui=context.getSingle(UI.class);
File selected=ui.showFileSelect(w,"Export script as",true,
new File(PlatformUtils.getDesktopFolder()),null,new String[] {".leafChatScript"},
"leafChat scripts");
if(selected==null) return;
if(!selected.getName().endsWith(".leafChatScript"))
{
selected=new File(selected.getPath()+".leafChatScript");
}
if(selected.exists())
{
ui.showUserError(w,"Export failed","Target file already exists");
return;
}
Script script=displayedScripts[scriptsUI.getSelectedIndex()];
try
{
IOUtils.copy(
new FileInputStream(script.getFile()),new FileOutputStream(selected),true);
}
catch(IOException e)
{
throw new GeneralException(e);
}
}
/**
* Callback: Edit script button.
* @throws GeneralException
*/
@UIAction
public void actionEditScript() throws GeneralException
{
Script script=displayedScripts[scriptsUI.getSelectedIndex()];
ScriptEditor editor=editors.get(script);
if(editor==null)
editors.put(script,new ScriptEditor(context,this,script));
else
editor.focus();
}
void informClosed(Script s)
{
editors.remove(s);
}
/**
* Callback: Delete script button.
* @throws GeneralException
*/
@UIAction
public void actionDeleteScript() throws GeneralException
{
Script script=displayedScripts[scriptsUI.getSelectedIndex()];
int value=context.getSingle(UI.class).showQuestion(w,"Confirm delete",
"Are you sure you want to delete the script <strong>"+
XML.esc(script.getName())+"</strong>? Deleted scripts cannot be restored.",
UI.BUTTON_YES|UI.BUTTON_CANCEL,"Delete script",null,null,UI.BUTTON_CANCEL);
if(value==UI.BUTTON_YES)
{
// See if we have an editor for that script, if we do it has to go
ScriptEditor editor=editors.get(script);
if(editor!=null) editor.closing();
((ScriptingPlugin)context.getPlugin()).deleteScript(script);
updateScripts();
}
}
/**
* Called by plugin when a script is marked changed or unchanged. We use it
* to grey out the Enabled checkbox and other items.
* @param script Script that is different
*/
public void informChanged(Script script)
{
if(w!=null && displayedScripts!=null)
{
for(int i=0;i<displayedScripts.length;i++)
{
if(displayedScripts[i]==script)
{
updateScriptDetails(i,script);
selectScripts();
return;
}
}
}
}
}