package org.geopublishing.geopublisher.swing; import java.awt.BorderLayout; import java.awt.Dimension; import java.io.BufferedWriter; import java.io.File; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JTabbedPane; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.log4j.Logger; import org.geopublishing.geopublisher.AtlasConfigEditable; import chrriis.common.UIUtils; import chrriis.common.WebServer; import chrriis.dj.nativeswing.swtimpl.NativeInterface; import chrriis.dj.nativeswing.swtimpl.components.HTMLEditorDirtyStateEvent; import chrriis.dj.nativeswing.swtimpl.components.HTMLEditorListener; import chrriis.dj.nativeswing.swtimpl.components.HTMLEditorSaveEvent; import chrriis.dj.nativeswing.swtimpl.components.JHTMLEditor; import chrriis.dj.nativeswing.swtimpl.components.JHTMLEditor.FCKEditorOptions; import chrriis.dj.nativeswing.swtimpl.components.JHTMLEditor.TinyMCEOptions; import chrriis.dj.nativeswing.swtimpl.components.JWebBrowser; import de.schmitzm.io.IOUtil; import de.schmitzm.lang.LangUtil; import de.schmitzm.swing.ExceptionDialog; /** * A html editor based on DJ native swing FCK JavaScript HTML Editor */ public class HTMLEditPaneJHTMLEditor extends HTMLEditPaneInterface implements HTMLEditorListener { private static final long serialVersionUID = -6452873259788176352L; private final Logger LOGGER = LangUtil.createLogger(this); /** * Type of (javascript) editor used in {@link JHTMLEditor} (currently * supported values: "FCK", "TinyMCE" */ protected String editorType = null; /** Holds a {@link JHTMLEditor} for each (language) page to edit. */ protected JTabbedPane tabs = null; /** Holds the source URL / File for each editing tab. */ protected Map<JHTMLEditor, URL> editURLs = new HashMap<JHTMLEditor, URL>(); private final AtlasConfigEditable ace; /** * Creates a new editor based on {@link JWebBrowser} with FCK. */ public HTMLEditPaneJHTMLEditor(AtlasConfigEditable ace) { this(null, ace); } /** * Creates a new editor based on {@link JWebBrowser}. * * @param editorType * type of editor (currently only "FCK" and "TinyMCE" is * supported); if <code>null</code> "FCK" is used */ public HTMLEditPaneJHTMLEditor(String editorType, AtlasConfigEditable ace) { setLayout(new BorderLayout()); this.ace = ace; if (editorType == null) editorType = "CK"; UIUtils.setPreferredLookAndFeel(); if (!NativeInterface.isOpen()) { NativeInterface.open(); new Thread(new Runnable() { public void run() { NativeInterface.runEventPump(); } }).start(); } this.editorType = editorType; this.tabs = new JTabbedPane(); this.tabs.setTabPlacement(JTabbedPane.TOP); this.add(tabs, BorderLayout.CENTER); this.setPreferredSize(new Dimension(800, 500)); } /** * Returns {@code this}. */ @Override public JComponent getComponent() { return this; } /** * Returns {@code true}, because {@link JHTMLEditor} already provides * scrolling. */ @Override public boolean hasScrollPane() { return true; } /** * Adds a tab pane to edit a HTML document. * * @param title * tab title * @param url * URL of the document to be edit * @param idx * index number for the document title if a new file is created */ @Override public void addEditorTab(String title, URL url, int idx) { JHTMLEditor editor = createJHTMLEditor(editorType, url); // add a listener for the save operation editor.addHTMLEditorListener(this); // add source file to map (for the new editor tab) editURLs.put(editor, url); String htmlContent = IOUtil.readURLasString(url); // Pattern p = Pattern.compile("(<img src=\")(.*?)\""); // htmlContent = StringEscapeUtils.unescapeHtml(htmlContent); // Matcher m = p.matcher(htmlContent); // while(m.find()){ // File folder = new File(url.getFile()).getParentFile(); // File image = new File(folder.getAbsolutePath()+"/"+m.group(2)); // if(image.exists()){ // String newpath = ace.getBrowserURLString(image); // htmlContent = htmlContent.replaceAll(m.group(2),newpath); // } // } editor.setHTMLContent(htmlContent); tabs.addTab(title, editor); } /** * Removes all tabs. */ @Override public void removeAllTabs() { editURLs.clear(); tabs.removeAll(); } /** * Called, when SAVE button of {@link JHTMLEditor} (of any tab!) is * performed. */ @Override public void saveHTML(HTMLEditorSaveEvent event) { saveHTML(event.getHTMLEditor(), null, null); } private void showSuccessMessage(Vector<String> copiedFiles, Vector<String> deletedFiles) { if (deletedFiles != null && deletedFiles.size() > 0) { JOptionPane.showMessageDialog(this, GpSwingUtil.R("HTMLEditPaneJHTMLEditor.save.deleted", deletedFiles.get(0)), GpSwingUtil .R("HTMLEditPaneJHTMLEditor.save.title"), JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog( this, copiedFiles.size() == 0 ? GpSwingUtil .R("HTMLEditPaneJHTMLEditor.save.success") : GpSwingUtil.R( "HTMLEditPaneJHTMLEditor.save.success2", copiedFiles.size(), "\n- " + LangUtil.stringConcatWithSep( "\n- ", (Collection) copiedFiles)), GpSwingUtil.R("HTMLEditPaneJHTMLEditor.save.title"), JOptionPane.INFORMATION_MESSAGE); } } /** * Saves the content of the given editor to its source. * * @param editor * the editor * @param collectCopiedFiles * stores names of the image files copied to map folder (can be * {@code null}, if not {@code null} the copied files are only * stored in the list and a success message is NOT given) * @param deletedFiles * Stores the names of the files deleted because they were empty. */ protected void saveHTML(JHTMLEditor editor, Vector<String> collectCopiedFiles, Vector<String> deletedFiles) { URL sourceURL = editURLs.get(editor); String htmlContent = editor.getHTMLContent(); if (htmlContent == null) { LOGGER.warn(JHTMLEditor.class.getSimpleName() + " asked to save a NULL HTML content! Ignoring..."); return; } if (LangUtil.removeWhitespacesToNull(htmlContent) == null) { // Delete the empty file. File sourceHTMLFile = IOUtil.urlToFile(sourceURL); sourceHTMLFile.delete(); if (deletedFiles != null) deletedFiles.add(sourceHTMLFile.getAbsolutePath()); return; } BufferedWriter writer = null; try { File sourceHTMLFile = IOUtil.urlToFile(sourceURL); // parse the html content for image references to the // local file system, Map<String, File> replaceRef = GpSwingUtil .findFileReferencesToReplace(htmlContent); // copy references to the image folder and replace // the reference in the html content Vector<String> copiedFiles = (collectCopiedFiles != null) ? collectCopiedFiles : new Vector<String>(); for (String absImageRef : replaceRef.keySet()) { File sourceRefFile = replaceRef.get(absImageRef); // replace html content with relative URL String relImageRef = "images/" + sourceRefFile.getName(); htmlContent = htmlContent.replace(absImageRef, relImageRef); // copy file File destRefFile = new File(sourceHTMLFile.getParent(), relImageRef); if (!destRefFile.equals(sourceRefFile)) { IOUtil.copyFile(LOGGER, sourceRefFile, destRefFile, false); copiedFiles.add(sourceRefFile.getName()); } } // replace editor content with reworked content editor.setHTMLContent(htmlContent); // write html file // Explicitly writing the HTML file in UTF-8 - of course the HTML // should only use ü etc. and no special characters anyways.. // BUT! Laotic &'...; tags are converted by FCKEditor no real UTF! // SO we have to force saving the html as real UTF8 -even on // windows. if (!htmlContent.startsWith("<html>")) { htmlContent = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body>" + htmlContent + "</body></html>"; } FileUtils.writeStringToFile(sourceHTMLFile, htmlContent, "UTF-8"); // To avoid multiple "success" dialogs when method is // called for all editors the dialog is only shown // if no global list is given if (collectCopiedFiles == null) { fireChangeEvents(); showSuccessMessage(copiedFiles, deletedFiles); } } catch (Exception err) { ExceptionDialog.show(editor, err, GpSwingUtil.R("HTMLEditPaneJHTMLEditor.save.title"), GpSwingUtil.R("HTMLEditPaneJHTMLEditor.save.error")); } finally { IOUtil.closeWriter(writer); } } /** * Called when surrounded window/dialog/application is closed. Should * perform editor specific actions (e.g. save operation). * * @param source * object which initiates the closing * @return {@code false} if possible dialog is canceled; {@code true} * otherwise to force the surrounding application to close the * editor frame */ @Override public boolean performClosing(Object source) { // check whether one of the files is not already saved Vector<JHTMLEditor> changedURLs = new Vector<JHTMLEditor>(); for (JHTMLEditor editor : editURLs.keySet()) { URL url = editURLs.get(editor); // TODO: Unfortunately the compare between file and content is // ever unequal! Probably because readURLasString(.) does // manually inserts "\n" for line breaks. if (editor.getHTMLContent() != null && !editor.getHTMLContent().equals( IOUtil.readURLasString(url))) changedURLs.add(editor); } // in case of unsaved changes, ask for save if (!changedURLs.isEmpty()) { int ret = JOptionPane.showConfirmDialog(this, GpSwingUtil.R("HTMLEditPaneJHTMLEditor.SaveQuestion")); switch (ret) { case JOptionPane.CANCEL_OPTION: case JOptionPane.DEFAULT_OPTION: // ESC // do nothing, just return FALSE to NOT close the dialog return false; case JOptionPane.YES_OPTION: // save changed files Vector<String> copiedFiles = new Vector<String>(); Vector<String> deletedFiles = new Vector<String>(); for (JHTMLEditor editor : changedURLs) saveHTML(editor, copiedFiles, deletedFiles); fireChangeEvents(); showSuccessMessage(copiedFiles, deletedFiles); return true; case JOptionPane.NO_OPTION: // do not save the changes, just return TRUE to close the dialog return true; } } return true; } /** * Creates an {@link JHTMLEditor} instance. * * @param editorType * supported (javascript) editors: "FCK", "TinyMCE" */ protected JHTMLEditor createJHTMLEditor(String editorType, URL sourceURL) { JHTMLEditor htmlEditor = null; /** * A File pointing to the local directory where to start the IMage/File * browser Swing Dialog */ File browseStartupFolder = IOUtil.urlToFile(sourceURL).getParentFile(); /** * The base url for the WebBrowser of the FCK HTML Editor */ String baseHrefStr = null; try { baseHrefStr = ace.getBrowserURLString(IOUtil .getParentUrl(sourceURL)); } catch (Exception err) { LOGGER.warn("Could not determine parent URL for '" + sourceURL + "'"); } // // String awcAbsURLStr = IOUtil.fileToURL( // ace.getAtlasDir().getAbsoluteFile()).toString(); // String sourceAbsURLStr = IOUtil.getParentUrl(sourceURL).toString(); // int relURLStartIdx = sourceAbsURLStr.indexOf(awcAbsURLStr); // String sourceRelURLStr = sourceAbsURLStr.substring(relURLStartIdx // + awcAbsURLStr.length()); // baseURLStr = "http://localhost:" + Webserver.PORT + "/" // + sourceRelURLStr; // if (!baseURLStr.endsWith("/")) // baseURLStr += "/"; // // // Startup folder for file chooser // // = Directory of the html file // browseStartupFolder = IOUtil.urlToFile(sourceURL).getParentFile(); // } catch (Exception err) { // LOGGER.warn("Could not determine parent URL for '" + sourceURL // + "'"); // } if (editorType.equalsIgnoreCase("CK")) { Map<String, String> optionMap = new HashMap<String, String>(); optionMap.put("autoGrow_onStartup", "'true'"); optionMap.put("baseHref", "'" + baseHrefStr + "'"); optionMap.put("extraPlugins", "'filebrowser'"); optionMap.put("width","'850'"); optionMap.put("filebrowserBrowseUrl", "'filemanager/browser/default/browser.html'"); optionMap.put("toolbar", "'basic'"); /** * config.toolbar_Full = [ { name: 'document', items : [ 'Source','-','Save','NewPage','DocProps','Preview','Print','-','Templates' ] }, { name: 'clipboard', items : [ 'Cut','Copy','Paste','PasteText','PasteFromWord','-','Undo','Redo' ] }, { name: 'editing', items : [ 'Find','Replace','-','SelectAll','-','SpellChecker', 'Scayt' ] }, { name: 'forms', items : [ 'Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField' ] }, '/', { name: 'basicstyles', items : [ 'Bold','Italic','Underline','Strike','Subscript','Superscript','-','RemoveFormat' ] }, { name: 'paragraph', items : [ 'NumberedList','BulletedList','-','Outdent','Indent','-','Blockquote','CreateDiv','-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock','-','BidiLtr','BidiRtl' ] }, { name: 'links', items : [ 'Link','Unlink','Anchor' ] }, { name: 'insert', items : [ 'Image','Flash','Table','HorizontalRule','Smiley','SpecialChar','PageBreak' ] }, '/', { name: 'styles', items : [ 'Styles','Format','Font','FontSize' ] }, { name: 'colors', items : [ 'TextColor','BGColor' ] }, { name: 'tools', items : [ 'Maximize', 'ShowBlocks','-','About' ] } ]; */ htmlEditor = new JHTMLEditor( JHTMLEditor.HTMLEditorImplementation.CKEditor, JHTMLEditor.CKEditorOptions.setOptions(optionMap)); htmlEditor.setFileBrowserStartFolder(browseStartupFolder); return htmlEditor; } if (editorType.equalsIgnoreCase("FCK")) { // Create FCK as editor String configScript = ""; // Configure toolbars // Also possible actions (but not useful for GP: // 'Style', 'Flash' // 'Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', // 'Button', 'ImageButton', 'HiddenField' configScript += "FCKConfig.ToolbarSets[\"Default\"] = [\n" + "['Source','DocProps','-','Save','NewPage','Preview','-','Templates'],\n" + "['Cut','Copy','Paste','PasteText','PasteWord','-','Print','SpellCheck'],\n" + "['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],\n" + "'/',\n" + "['FontFormat','FontName','FontSize'],\n" + "['TextColor','BGColor'],\n" + "'/',\n" + "['Bold','Italic','Underline','StrikeThrough','-','Subscript','Superscript'],\n" + "['OrderedList','UnorderedList','-','Outdent','Indent','Blockquote'],\n" + "['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],\n" + "['Link','Unlink','Anchor'],\n" + "['Image','Table','Rule','Smiley','SpecialChar','PageBreak', '-', 'ShowBlocks'],\n" + "];\n" + "FCKConfig.ToolbarCanCollapse = false;\n"; // Configure base URL so that the images with relative URLs are // also shown if (baseHrefStr != null) configScript += "FCKConfig.BaseHref = '" + baseHrefStr + "';\n"; // Hide "target" options for links because in GP // we can only show links in the same frame configScript += "FCKConfig.LinkDlgHideTarget = true;\n"; // Hide complete "link" tab for images to avoid // the "target" property (see above!) configScript += "FCKConfig.ImageDlgHideLink = true;\n"; // Set starting focus to editing area configScript += "FCKConfig.StartupFocus = true;\n"; // // Set auto formatting html code (on output = saving) // configScript += // "FCKConfig.FormatOutput = true; FCKConfig.FormatSource = true;\n"; // Set language used in GP configScript += "FCKConfig.AutoDetectLanguage = false; FCKConfig.DefaultLanguage = \"" + Locale.getDefault() + "\";\n"; // Disable Upload buttons configScript += "FCKConfig.ImageUpload = false; FCKConfig.LinkUpload = false;\n"; // Disable Browse buttons for links configScript += "FCKConfig.LinkBrowser = false;\n"; // We use our own file chooser (hack in DJ WebServer), so the // unfortunately // necessary browser window should be as small as possible! configScript += "FCKConfig.ImageBrowserWindowWidth = \"1\"; FCKConfig.ImageBrowserWindowHeight = \"1\";\n"; configScript += "FCKConfig.AdditionalNumericEntities = \"[^ 0-9A-z\\d():\\-]\" ;\n"; // configScript += // "FCKConfig.AdditionalNumericEntities = \"[^ A-z\\d]\" ;\n"; // Use of a custom browser (does only work if this // runs on the same web server as FCK! // configScript += "FCKConfig.ImageBrowserURL = \"" // + myimageBrowserUrl + "\";\n"; // configScript += // "FCKConfig.ImageBrowserURL = FCKConfig.BasePath + 'filemanager/browser/default/browser.html?Connector=../../connectors/' + _FileBrowserLanguage + '/connector.' + _FileBrowserExtension;\n"; // // Enable FCK debugging // configScript += "FCKConfig.debug = true;\n"; // Log the editor configuration LOGGER.debug(configScript); // Create editor instance htmlEditor = new JHTMLEditor( JHTMLEditor.HTMLEditorImplementation.FCKEditor, FCKEditorOptions .setCustomJavascriptConfiguration(configScript)); htmlEditor.setFileBrowserStartFolder(browseStartupFolder); return htmlEditor; } if (editorType.equalsIgnoreCase("TinyMCE")) { // Create TinyMCE as editor Map<String, String> optionMap = new HashMap<String, String>(); optionMap .put("theme_advanced_buttons1", "'bold,italic,underline,strikethrough,sub,sup,|,charmap,|,justifyleft,justifycenter,justifyright,justifyfull,|,hr,removeformat'"); optionMap .put("theme_advanced_buttons2", "'undo,redo,|,cut,copy,paste,pastetext,pasteword,|,search,replace,|,forecolor,backcolor,bullist,numlist,|,outdent,indent,blockquote,|,table,link,image'"); optionMap.put("theme_advanced_buttons3", "''"); optionMap.put("theme_advanced_toolbar_location", "'top'"); optionMap.put("theme_advanced_toolbar_align", "'left'"); // Language can be configured when language packs are added to the // classpath. Language packs can be found here: // http://tinymce.moxiecode.com/download_i18n.php // optionMap.put("language", "'de'"); optionMap.put("plugins", "'table,paste,contextmenu,searchreplace,media'"); optionMap.put("image_advtab", "true"); /** * selector: "textarea", theme: "modern", plugins: [ * "advlist autolink lists link image charmap print preview hr anchor pagebreak" * , * "searchreplace wordcount visualblocks visualchars code fullscreen" * , * "insertdatetime media nonbreaking save table contextmenu directionality" * , "emoticons template paste textcolor moxiemanager" ], toolbar1: * "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link code image | forecolor backcolor emoticons" * , image_advtab: true, statusbar : false, menubar : false, */ htmlEditor = new JHTMLEditor( JHTMLEditor.HTMLEditorImplementation.TinyMCE, JHTMLEditor.TinyMCEOptions.setOptions(optionMap)); return htmlEditor; } throw new UnsupportedOperationException( "Unknown editor type to create JHTMLEditor: " + editorType); } @Override public void notifyDirtyStateChanged(HTMLEditorDirtyStateEvent arg0) { LOGGER.info(arg0); } @Override public Dimension getPreferredSize() { return null; } }