/* * Christopher Deckers (chrriis@nextencia.net) * http://www.nextencia.net * * See the file "readme.txt" for information on usage and redistribution of * this file, and for a DISCLAIMER OF ALL WARRANTIES. */ package chrriis.dj.nativeswing.swtimpl.components; import java.awt.BorderLayout; import java.io.File; import java.net.MalformedURLException; import java.util.EventListener; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; import chrriis.common.ObjectRegistry; import chrriis.common.Utils; import chrriis.common.WebServer; import chrriis.common.WebServer.HTTPRequest; import chrriis.common.WebServer.WebServerContent; import chrriis.dj.nativeswing.NSOption; import chrriis.dj.nativeswing.swtimpl.EventDispatchUtils; import chrriis.dj.nativeswing.swtimpl.LocalMessage; import chrriis.dj.nativeswing.swtimpl.NSPanelComponent; /** * An HTML editor. It is a browser-based component, which relies on the * FCKeditor, CKEditor or the TinyMCE editor.<br/> * Methods execute when this component is initialized. If the component is not * initialized, methods will be executed as soon as it gets initialized. If the * initialization fails, the methods will not have any effect. The results from * methods have relevant values only when the component is valid. * * @author Christopher Deckers * @author Jörn Heid (TinyMCE implementation) */ public class JHTMLEditor extends NSPanelComponent { static interface JHTMLEditorImplementation { public WebServerContent getWebServerContent(HTTPRequest httpRequest, String resourcePath, final int instanceID); public String getHTMLContent(); public void setHTMLContent(String html); public void setDirtyTrackingActive(boolean isDirtyTrackingActive); public void clearDirtyIndicator(); } public static enum HTMLEditorImplementation { FCKEditor, CKEditor, TinyMCE }; public static class TinyMCEOptions { private TinyMCEOptions() { } static final String SET_CUSTOM_HTML_HEADERS_OPTION_KEY = "TinyMCE Custom HTML Headers"; /** * Set custom HTML headers, which is mostly useful when integrating * certain TinyMCE plugins. */ public static NSOption setCustomHTMLHeaders( final String customHTMLHeaders) { return new NSOption(SET_CUSTOM_HTML_HEADERS_OPTION_KEY) { @Override public Object getOptionValue() { return customHTMLHeaders; } }; } static final String SET_OPTIONS_OPTION_KEY = "TinyMCE Options"; /** * Create an option to set TinyMCE editor options.<br/> * The list of possible options to set for TinyMCE can be found here: <a * href * ="http://wiki.moxiecode.com/index.php/TinyMCE:Configuration">http: * //wiki.moxiecode.com/index.php/TinyMCE:Configuration</a>. * * @param optionMap * a map containing the key/value pairs accepted by TinyMCE. * @return the option to set the options. */ public static NSOption setOptions(Map<String, String> optionMap) { final Map<String, String> optionMap_ = new HashMap<String, String>( optionMap); return new NSOption(SET_OPTIONS_OPTION_KEY) { @Override public Object getOptionValue() { return optionMap_; } }; } } public static class FCKEditorOptions { private FCKEditorOptions() { } static final String SET_CUSTOM_JAVASCRIPT_CONFIGURATION_OPTION_KEY = "FCKEditor Custom Configuration Script"; /** * Create an option to set custom Javascript configuration for the * FCKeditor editor.<br/> * The list of possible options to set for FCKeditor can be found here: * <a href= * "http://docs.fckeditor.net/FCKeditor_2.x/Developers_Guide/Configuration/Configuration_Options" * >http://docs.fckeditor.net/FCKeditor_2.x/Developers_Guide/ * Configuration/Configuration_Options</a>.<br/> * * @param javascriptConfiguration * the javascript configuration. * @return the option to set a custom configuration. */ public static NSOption setCustomJavascriptConfiguration( final String javascriptConfiguration) { return new NSOption(SET_CUSTOM_JAVASCRIPT_CONFIGURATION_OPTION_KEY) { @Override public Object getOptionValue() { return javascriptConfiguration; } }; } } public static class CKEditorOptions { private CKEditorOptions() { } static final String SET_OPTIONS_OPTION_KEY = "CKEditor Options"; /** * Create an option to set CKEditor editor options.<br/> * The list of possible options to set for CKEditor can be found here: * <a href= * "http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.config.html" * >http * ://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.config.html</a>. * * @param optionMap * a map containing the key/value pairs accepted by CKEditor. * @return the option to set the options. */ public static NSOption setOptions(Map<String, String> optionMap) { final Map<String, String> optionMap_ = new HashMap<String, String>( optionMap); return new NSOption(SET_OPTIONS_OPTION_KEY) { @Override public Object getOptionValue() { return optionMap_; } }; } } private JWebBrowser webBrowser; private int instanceID; private JHTMLEditorImplementation implementation; JHTMLEditorImplementation getImplementation() { return implementation; } /** * Construct an HTML editor. * * @param options * the options to configure the behavior of this component. */ public JHTMLEditor(HTMLEditorImplementation editorImplementation, NSOption... options) { if (editorImplementation == null) { throw new NullPointerException( "The editor implementation cannot be null!"); } Map<Object, Object> optionMap = NSOption.createOptionMap(options); webBrowser = new JWebBrowser(options); initialize(webBrowser.getNativeComponent()); switch (editorImplementation) { case FCKEditor: try { implementation = new JHTMLEditorFCKeditor(this, optionMap); break; } catch (RuntimeException e) { if (editorImplementation != null) { throw e; } } case CKEditor: try { implementation = new JHTMLEditorCKeditor(this, optionMap); break; } catch (RuntimeException e) { if (editorImplementation != null) { throw e; } } case TinyMCE: try { implementation = new JHTMLEditorTinyMCE(this, optionMap); break; } catch (RuntimeException e) { if (editorImplementation != null) { throw e; } } default: throw new IllegalStateException( "A suitable HTML editor (FCKeditor, CKeditor, TinyMCE) distribution could not be found on the classpath!"); } webBrowser.addWebBrowserListener(new WebBrowserAdapter() { @Override public void commandReceived(WebBrowserCommandEvent e) { String command = e.getCommand(); if ("[Chrriis]JH_setLoaded".equals(command)) { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == InitializationListener.class) { ((InitializationListener) listeners[i + 1]) .objectInitialized(); } } } else if ("[Chrriis]JH_setDirty".equals(command)) { setDirty(true); } } }); webBrowser.setBarsVisible(false); add(webBrowser, BorderLayout.CENTER); instanceID = ObjectRegistry.getInstance().add(this); final AtomicBoolean result = new AtomicBoolean(); InitializationListener initializationListener = new InitializationListener() { public void objectInitialized() { removeInitializationListener(this); result.set(true); } }; addInitializationListener(initializationListener); webBrowser.navigate(WebServer.getDefaultWebServer() .getDynamicContentURL(JHTMLEditor.class.getName(), String.valueOf(instanceID), "index.html")); webBrowser.getNativeComponent().runSync(new LocalMessage() { @Override public Object run(Object[] args) { InitializationListener initializationListener = (InitializationListener) args[0]; final AtomicBoolean result = (AtomicBoolean) args[1]; EventDispatchUtils.sleepWithEventDispatch( new EventDispatchUtils.Condition() { public boolean getValue() { return result.get(); } }, 4000); removeInitializationListener(initializationListener); return null; } }, initializationListener, result); } /** * Get the web browser that contains this component. The web browser should * only be used to add listeners, for example to listen to window creation * events. * * @return the web browser. */ public JWebBrowser getWebBrowser() { return webBrowser; } // MS-Hack.sn protected File fileBrowserStartFolder = null; public void setFileBrowserStartFolder(File folder) { fileBrowserStartFolder = folder; } public File getFileBrowserStartFolder() { return fileBrowserStartFolder; } // public String getFileBrowserStartFolderStr() { // URL url = getFileBrowserStartFolder(); // if ( url == null ) // return ""; // String urlStr = url.getPath(); // if ( !urlStr.endsWith("/") ) // urlStr += "/"; // return urlStr; // } // MS-Hack.en protected static WebServerContent getWebServerContent( final HTTPRequest httpRequest) { String resourcePath = httpRequest.getResourcePath(); int index = resourcePath.indexOf('/'); int instanceID = Integer.parseInt(resourcePath.substring(0, index)); JHTMLEditor htmlEditor = (JHTMLEditor) ObjectRegistry.getInstance() .get(instanceID); if (htmlEditor == null) { return null; } // // MS-Hack.sn // String currFolder = httpRequest.getQueryParameterMap().get( // "CurrentFolder"); // if (currFolder != null && // currFolder.equals("/") && // htmlEditor.getFileBrowserStartFolder() != null) { // httpRequest.getQueryParameterMap().put("CurrentFolder",htmlEditor.getFileBrowserStartFolderStr()); // } // if ( "".equals(currFolder) ) { // currFolder = "/"; // httpRequest.getQueryParameterMap().put("CurrentFolder",currFolder); // } // // MS-Hack.en String resourcePath_ = resourcePath.substring(index + 1); if (resourcePath_.startsWith("/")) { resourcePath_ = resourcePath_.substring(1); } return htmlEditor.getWebServerContent(httpRequest, resourcePath_, instanceID); } /** * Serve the HTTP content requested by the editor web page, which can be * altered by subclasses. Note that altering the default content is * generally not needed and is not recommended. * * @return the content. */ protected WebServerContent getWebServerContent(HTTPRequest httpRequest, String resourcePath, final int instanceID) { return implementation.getWebServerContent(httpRequest, resourcePath, instanceID); } /** * Get the HTML content. * * @return the HTML content. */ public String getHTMLContent() { return convertLinksToLocal(implementation.getHTMLContent()); } /** * Set the HTML content. * * @param html * the HTML content. */ public void setHTMLContent(String html) { html = JHTMLEditor .convertLinksFromLocal(html.replaceAll("[\r\n]", " ")); implementation.setHTMLContent(html); setDirty(false); } private boolean isDirty; /** * Indicate whether the editor is dirty, which means its content has changed * since it was last set or the dirty state was cleared. * * @return true if the editor is dirty, false otherwise. */ public boolean isDirty() { return isDirty; } private void setDirty(boolean isDirty) { if (this.isDirty == isDirty) { return; } this.isDirty = isDirty; Object[] listeners = listenerList.getListenerList(); HTMLEditorDirtyStateEvent ev = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == HTMLEditorListener.class) { if (ev == null) { ev = new HTMLEditorDirtyStateEvent(JHTMLEditor.this, isDirty); } ((HTMLEditorListener) listeners[i + 1]) .notifyDirtyStateChanged(ev); } } } /** * Clear the dirty state. */ public void clearDirtyState() { implementation.clearDirtyIndicator(); setDirty(false); } static String convertLinksToLocal(String html) { if (html == null) { return html; } // Transform proxied URLs to "file:///". Pattern p = Pattern.compile("=\\s*\"(" + WebServer.getDefaultWebServer().getURLPrefix() + "/resource/)([^/]+)/([^\"]+)\"\\s"); for (Matcher m; (m = p.matcher(html)).find();) { String codeBase = html.substring(m.start(2), m.end(2)); String resource = html.substring(m.start(3), m.end(3)); try { resource = new File(Utils.decodeURL(Utils.decodeURL(codeBase)), resource).toURI().toURL().toExternalForm(); } catch (MalformedURLException e) { } html = html.substring(0, m.start(1)) + resource + html.substring(m.end(3)); } return html; } static String convertLinksFromLocal(String html) { if (html == null) { return html; } // Transform "file:///" to proxied URLs. Pattern p = Pattern.compile("=\\s*\"(file:/{1,3})([^\"]+)\"\\s"); for (Matcher m; (m = p.matcher(html)).find();) { String resource = html.substring(m.start(2), m.end(2)); File resourceFile = new File(resource); resource = WebServer.getDefaultWebServer().getResourcePathURL( Utils.encodeURL(resourceFile.getParent()), resourceFile.getName()); html = html.substring(0, m.start(1)) + resource + html.substring(m.end(2)); } return html; } /** * Add an HTML editor listener. * * @param listener * The HTML editor listener to add. */ public void addHTMLEditorListener(HTMLEditorListener listener) { listenerList.add(HTMLEditorListener.class, listener); } /** * Remove an HTML editor listener. * * @param listener * the HTML editor listener to remove. */ public void removeHTMLEditorListener(HTMLEditorListener listener) { listenerList.remove(HTMLEditorListener.class, listener); } /** * Get the HTML editor listeners. * * @return the HTML editor listeners. */ public HTMLEditorListener[] getHTMLEditorListeners() { return listenerList.getListeners(HTMLEditorListener.class); } private static interface InitializationListener extends EventListener { public void objectInitialized(); } private void addInitializationListener(InitializationListener listener) { listenerList.add(InitializationListener.class, listener); } private void removeInitializationListener(InitializationListener listener) { listenerList.remove(InitializationListener.class, listener); } // private InitializationListener[] getInitializationListeners() { // return listenerList.getListeners(InitializationListener.class); // } }